about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/default.nix5
-rw-r--r--nixos/doc/manual/README13
-rw-r--r--nixos/doc/manual/administration/service-mgmt.xml98
-rw-r--r--nixos/doc/manual/configuration/configuration.xml2
-rw-r--r--nixos/doc/manual/configuration/gpu-accel.xml76
-rw-r--r--nixos/doc/manual/configuration/linux-kernel.xml2
-rw-r--r--nixos/doc/manual/configuration/networking.xml1
-rw-r--r--nixos/doc/manual/configuration/profiles/hardened.xml10
-rw-r--r--nixos/doc/manual/configuration/profiles/qemu-guest.xml5
-rw-r--r--nixos/doc/manual/configuration/renaming-interfaces.xml67
-rw-r--r--nixos/doc/manual/configuration/subversion.xml140
-rw-r--r--nixos/doc/manual/configuration/user-mgmt.xml2
-rw-r--r--nixos/doc/manual/configuration/wayland.xml23
-rw-r--r--nixos/doc/manual/configuration/x-windows.xml2
-rw-r--r--nixos/doc/manual/contributing-to-this-manual.xml22
-rw-r--r--nixos/doc/manual/default.nix1
-rw-r--r--nixos/doc/manual/development/meta-attributes.xml2
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.xml4
-rw-r--r--nixos/doc/manual/installation/installing-from-other-distro.xml7
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.xml7
-rw-r--r--nixos/doc/manual/installation/installing.xml9
-rw-r--r--nixos/doc/manual/installation/upgrading.xml16
-rw-r--r--nixos/doc/manual/man-nixos-install.xml23
-rw-r--r--nixos/doc/manual/manual.xml1
-rw-r--r--nixos/doc/manual/preface.xml6
-rw-r--r--nixos/doc/manual/release-notes/release-notes.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml11
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.xml904
-rw-r--r--nixos/doc/manual/release-notes/rl-2103.xml99
-rw-r--r--nixos/doc/manual/release-notes/rl-2105.xml729
-rwxr-xr-xnixos/doc/varlistentry-fixer.rb74
-rw-r--r--nixos/lib/build-vms.nix4
-rw-r--r--nixos/lib/make-disk-image.nix71
-rw-r--r--nixos/lib/make-iso9660-image.nix2
-rw-r--r--nixos/lib/make-system-tarball.nix2
-rw-r--r--nixos/lib/test-driver/test-driver.py8
-rw-r--r--nixos/lib/testing-python.nix267
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix2
-rwxr-xr-xnixos/maintainers/scripts/ec2/create-amis.sh56
-rw-r--r--nixos/modules/config/console.nix23
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix2
-rw-r--r--nixos/modules/config/fonts/fontdir.nix2
-rw-r--r--nixos/modules/config/gnu.nix10
-rw-r--r--nixos/modules/config/i18n.nix2
-rw-r--r--nixos/modules/config/ldap.nix35
-rw-r--r--nixos/modules/config/malloc.nix2
-rw-r--r--nixos/modules/config/networking.nix4
-rw-r--r--nixos/modules/config/no-x-libs.nix4
-rw-r--r--nixos/modules/config/pulseaudio.nix12
-rw-r--r--nixos/modules/config/swap.nix4
-rw-r--r--nixos/modules/config/system-path.nix5
-rw-r--r--nixos/modules/config/update-users-groups.pl30
-rw-r--r--nixos/modules/config/users-groups.nix31
-rw-r--r--nixos/modules/config/xdg/portal.nix2
-rw-r--r--nixos/modules/config/zram.nix20
-rw-r--r--nixos/modules/hardware/acpilight.nix1
-rw-r--r--nixos/modules/hardware/all-firmware.nix1
-rw-r--r--nixos/modules/hardware/device-tree.nix8
-rw-r--r--nixos/modules/hardware/i2c.nix43
-rw-r--r--nixos/modules/hardware/keyboard/zsa.nix27
-rw-r--r--nixos/modules/hardware/network/ath-user-regd.nix31
-rw-r--r--nixos/modules/hardware/nitrokey.nix16
-rw-r--r--nixos/modules/hardware/opentabletdriver.nix69
-rw-r--r--nixos/modules/hardware/rtl-sdr.nix20
-rw-r--r--nixos/modules/hardware/sensor/hddtemp.nix81
-rw-r--r--nixos/modules/hardware/video/bumblebee.nix2
-rw-r--r--nixos/modules/hardware/video/nvidia.nix133
-rw-r--r--nixos/modules/hardware/video/switcheroo-control.nix18
-rw-r--r--nixos/modules/i18n/input-method/default.nix3
-rw-r--r--nixos/modules/i18n/input-method/default.xml4
-rw-r--r--nixos/modules/i18n/input-method/fcitx5.nix33
-rw-r--r--nixos/modules/i18n/input-method/hime.nix18
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix9
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-aarch64.nix32
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix32
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image.nix9
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix4
-rw-r--r--nixos/modules/installer/netboot/netboot.nix2
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix8
-rw-r--r--nixos/modules/installer/tools/nixos-build-vms/build-vms.nix2
-rw-r--r--nixos/modules/installer/tools/nixos-enter.sh3
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl27
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh2
-rw-r--r--nixos/modules/installer/tools/nixos-option/default.nix10
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh506
-rw-r--r--nixos/modules/installer/tools/tools.nix115
-rw-r--r--nixos/modules/misc/crashdump.nix3
-rw-r--r--nixos/modules/misc/documentation.nix38
-rw-r--r--nixos/modules/misc/ids.nix24
-rw-r--r--nixos/modules/misc/locate.nix69
-rw-r--r--nixos/modules/misc/nixpkgs.nix2
-rw-r--r--nixos/modules/module-list.nix58
-rw-r--r--nixos/modules/profiles/all-hardware.nix11
-rw-r--r--nixos/modules/profiles/hardened.nix7
-rw-r--r--nixos/modules/profiles/installation-device.nix4
-rw-r--r--nixos/modules/profiles/qemu-guest.nix4
-rw-r--r--nixos/modules/programs/appgate-sdp.nix23
-rw-r--r--nixos/modules/programs/bandwhich.nix2
-rw-r--r--nixos/modules/programs/captive-browser.nix88
-rw-r--r--nixos/modules/programs/cdemu.nix3
-rw-r--r--nixos/modules/programs/chromium.nix2
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.nix2
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.pl15
-rw-r--r--nixos/modules/programs/firejail.nix46
-rw-r--r--nixos/modules/programs/fish.nix301
-rw-r--r--nixos/modules/programs/msmtp.nix104
-rw-r--r--nixos/modules/programs/proxychains.nix165
-rw-r--r--nixos/modules/programs/ssh.nix2
-rw-r--r--nixos/modules/programs/ssmtp.nix22
-rw-r--r--nixos/modules/programs/tilp2.nix28
-rw-r--r--nixos/modules/programs/venus.nix173
-rw-r--r--nixos/modules/programs/vim.nix14
-rw-r--r--nixos/modules/programs/wshowkeys.nix22
-rw-r--r--nixos/modules/programs/x2goserver.nix2
-rw-r--r--nixos/modules/programs/xss-lock.nix2
-rw-r--r--nixos/modules/rename.nix3
-rw-r--r--nixos/modules/security/acme.nix143
-rw-r--r--nixos/modules/security/acme.xml46
-rw-r--r--nixos/modules/security/doas.nix11
-rw-r--r--nixos/modules/security/hidepid.nix4
-rw-r--r--nixos/modules/security/pam.nix57
-rw-r--r--nixos/modules/security/pam_mount.nix14
-rw-r--r--nixos/modules/security/rngd.nix64
-rw-r--r--nixos/modules/security/systemd-confinement.nix2
-rw-r--r--nixos/modules/security/wrappers/default.nix16
-rw-r--r--nixos/modules/security/wrappers/wrapper.c330
-rw-r--r--nixos/modules/security/wrappers/wrapper.nix21
-rw-r--r--nixos/modules/services/admin/salt/master.nix4
-rw-r--r--nixos/modules/services/admin/salt/minion.nix2
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix1
-rw-r--r--nixos/modules/services/audio/alsa.nix2
-rw-r--r--nixos/modules/services/audio/jack.nix3
-rw-r--r--nixos/modules/services/audio/mpd.nix135
-rw-r--r--nixos/modules/services/audio/mpdscribble.nix202
-rw-r--r--nixos/modules/services/audio/snapserver.nix28
-rw-r--r--nixos/modules/services/backup/bacula.nix23
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix4
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix3
-rw-r--r--nixos/modules/services/backup/restic.nix4
-rw-r--r--nixos/modules/services/backup/syncoid.nix52
-rw-r--r--nixos/modules/services/backup/tarsnap.nix17
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix7
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix5
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix12
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix74
-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.nix3
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix6
-rw-r--r--nixos/modules/services/continuous-integration/gocd-agent/default.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix3
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix105
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix11
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix35
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/default.nix6
-rw-r--r--nixos/modules/services/databases/couchdb.nix3
-rw-r--r--nixos/modules/services/databases/firebird.nix5
-rw-r--r--nixos/modules/services/databases/foundationdb.nix2
-rw-r--r--nixos/modules/services/databases/memcached.nix15
-rw-r--r--nixos/modules/services/databases/mongodb.nix9
-rw-r--r--nixos/modules/services/databases/neo4j.nix8
-rw-r--r--nixos/modules/services/databases/openldap.nix454
-rw-r--r--nixos/modules/services/databases/postgresql.nix15
-rw-r--r--nixos/modules/services/databases/redis.nix108
-rw-r--r--nixos/modules/services/databases/riak-cs.nix202
-rw-r--r--nixos/modules/services/databases/riak.nix2
-rw-r--r--nixos/modules/services/databases/stanchion.nix194
-rw-r--r--nixos/modules/services/databases/victoriametrics.nix2
-rw-r--r--nixos/modules/services/databases/virtuoso.nix5
-rw-r--r--nixos/modules/services/desktops/flatpak.nix14
-rw-r--r--nixos/modules/services/desktops/gnome3/evolution-data-server.nix54
-rw-r--r--nixos/modules/services/desktops/pipewire.nix41
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix340
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix265
-rw-r--r--nixos/modules/services/desktops/profile-sync-daemon.nix4
-rw-r--r--nixos/modules/services/desktops/telepathy.nix5
-rw-r--r--nixos/modules/services/development/bloop.nix2
-rw-r--r--nixos/modules/services/development/hoogle.nix12
-rw-r--r--nixos/modules/services/editors/emacs.xml10
-rw-r--r--nixos/modules/services/editors/infinoted.nix8
-rw-r--r--nixos/modules/services/games/factorio.nix11
-rw-r--r--nixos/modules/services/games/freeciv.nix187
-rw-r--r--nixos/modules/services/games/openarena.nix2
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix18
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix132
-rw-r--r--nixos/modules/services/hardware/fwupd.nix18
-rw-r--r--nixos/modules/services/hardware/lcd.nix5
-rw-r--r--nixos/modules/services/hardware/power-profiles-daemon.nix53
-rw-r--r--nixos/modules/services/hardware/sane.nix2
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix14
-rw-r--r--nixos/modules/services/hardware/thermald.nix20
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix260
-rw-r--r--nixos/modules/services/hardware/throttled.nix6
-rw-r--r--nixos/modules/services/hardware/tlp.nix2
-rw-r--r--nixos/modules/services/hardware/trezord.nix2
-rw-r--r--nixos/modules/services/hardware/udev.nix29
-rw-r--r--nixos/modules/services/hardware/undervolt.nix46
-rw-r--r--nixos/modules/services/hardware/xow.nix3
-rw-r--r--nixos/modules/services/logging/logstash.nix4
-rw-r--r--nixos/modules/services/logging/promtail.nix86
-rw-r--r--nixos/modules/services/logging/vector.nix61
-rw-r--r--nixos/modules/services/mail/dovecot.nix4
-rw-r--r--nixos/modules/services/mail/freepops.nix89
-rw-r--r--nixos/modules/services/mail/mailman.nix40
-rw-r--r--nixos/modules/services/mail/mailman.xml39
-rw-r--r--nixos/modules/services/mail/mlmmj.nix43
-rw-r--r--nixos/modules/services/mail/postfix.nix34
-rw-r--r--nixos/modules/services/mail/postgrey.nix2
-rw-r--r--nixos/modules/services/mail/roundcube.nix5
-rw-r--r--nixos/modules/services/mail/rspamd.nix48
-rw-r--r--nixos/modules/services/misc/autofs.nix1
-rw-r--r--nixos/modules/services/misc/autorandr.nix4
-rw-r--r--nixos/modules/services/misc/cfdyndns.nix22
-rw-r--r--nixos/modules/services/misc/cgminer.nix8
-rw-r--r--nixos/modules/services/misc/dictd.nix2
-rw-r--r--nixos/modules/services/misc/disnix.nix88
-rw-r--r--nixos/modules/services/misc/dysnomia.nix217
-rw-r--r--nixos/modules/services/misc/etebase-server.nix205
-rw-r--r--nixos/modules/services/misc/etesync-dav.nix92
-rw-r--r--nixos/modules/services/misc/exhibitor.nix2
-rw-r--r--nixos/modules/services/misc/felix.nix2
-rw-r--r--nixos/modules/services/misc/fstrim.nix2
-rw-r--r--nixos/modules/services/misc/gitea.nix59
-rw-r--r--nixos/modules/services/misc/gitit.nix1
-rw-r--r--nixos/modules/services/misc/gitlab.nix69
-rw-r--r--nixos/modules/services/misc/gitolite.nix2
-rw-r--r--nixos/modules/services/misc/gogs.nix8
-rw-r--r--nixos/modules/services/misc/home-assistant.nix6
-rw-r--r--nixos/modules/services/misc/ihaskell.nix1
-rw-r--r--nixos/modules/services/misc/jellyfin.nix40
-rw-r--r--nixos/modules/services/misc/klipper.nix59
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix8
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix5
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml5
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix3
-rw-r--r--nixos/modules/services/misc/n8n.nix78
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix12
-rw-r--r--nixos/modules/services/misc/nzbhydra2.nix78
-rw-r--r--nixos/modules/services/misc/octoprint.nix3
-rw-r--r--nixos/modules/services/misc/pykms.nix13
-rw-r--r--nixos/modules/services/misc/redmine.nix4
-rw-r--r--nixos/modules/services/misc/rippled.nix1
-rw-r--r--nixos/modules/services/misc/safeeyes.nix4
-rw-r--r--nixos/modules/services/misc/siproxd.nix18
-rw-r--r--nixos/modules/services/misc/snapper.nix2
-rw-r--r--nixos/modules/services/misc/svnserve.nix3
-rw-r--r--nixos/modules/services/misc/synergy.nix5
-rw-r--r--nixos/modules/services/misc/weechat.nix1
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix1
-rw-r--r--nixos/modules/services/misc/zookeeper.nix5
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix2
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix150
-rw-r--r--nixos/modules/services/monitoring/grafana.nix9
-rw-r--r--nixos/modules/services/monitoring/graphite.nix6
-rw-r--r--nixos/modules/services/monitoring/incron.nix2
-rw-r--r--nixos/modules/services/monitoring/loki.nix4
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix111
-rw-r--r--nixos/modules/services/monitoring/netdata.nix3
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix277
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix22
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bird.nix46
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/flow.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/json.nix28
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix10
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginx.nix7
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix51
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix53
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix121
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix78
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix60
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/sql.nix104
-rw-r--r--nixos/modules/services/monitoring/smartd.nix2
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix4
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix58
-rw-r--r--nixos/modules/services/monitoring/thanos.nix4
-rw-r--r--nixos/modules/services/monitoring/ups.nix2
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix12
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix17
-rw-r--r--nixos/modules/services/network-filesystems/netatalk.nix15
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix2
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix2
-rw-r--r--nixos/modules/services/network-filesystems/rsyncd.nix60
-rw-r--r--nixos/modules/services/network-filesystems/samba-wsdd.nix124
-rw-r--r--nixos/modules/services/network-filesystems/xtreemfs.nix21
-rw-r--r--nixos/modules/services/network-filesystems/yandex-disk.nix2
-rw-r--r--nixos/modules/services/networking/amuled.nix4
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix5
-rw-r--r--nixos/modules/services/networking/babeld.nix35
-rw-r--r--nixos/modules/services/networking/bee-clef.nix107
-rw-r--r--nixos/modules/services/networking/bee.nix149
-rw-r--r--nixos/modules/services/networking/bind.nix36
-rw-r--r--nixos/modules/services/networking/bitlbee.nix4
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix24
-rw-r--r--nixos/modules/services/networking/cjdns.nix2
-rw-r--r--nixos/modules/services/networking/cntlm.nix9
-rw-r--r--nixos/modules/services/networking/connman.nix3
-rw-r--r--nixos/modules/services/networking/consul.nix1
-rw-r--r--nixos/modules/services/networking/corerad.nix12
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix8
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix25
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix9
-rw-r--r--nixos/modules/services/networking/dnsdist.nix27
-rw-r--r--nixos/modules/services/networking/epmd.nix2
-rw-r--r--nixos/modules/services/networking/flashpolicyd.nix85
-rw-r--r--nixos/modules/services/networking/gateone.nix4
-rw-r--r--nixos/modules/services/networking/gogoclient.nix2
-rw-r--r--nixos/modules/services/networking/gvpe.nix8
-rw-r--r--nixos/modules/services/networking/hostapd.nix5
-rw-r--r--nixos/modules/services/networking/hylafax/modem-default.nix6
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix20
-rw-r--r--nixos/modules/services/networking/hylafax/systemd.nix28
-rw-r--r--nixos/modules/services/networking/icecream/daemon.nix155
-rw-r--r--nixos/modules/services/networking/icecream/scheduler.nix101
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix8
-rw-r--r--nixos/modules/services/networking/iwd.nix5
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix12
-rw-r--r--nixos/modules/services/networking/kippo.nix14
-rw-r--r--nixos/modules/services/networking/kresd.nix16
-rw-r--r--nixos/modules/services/networking/mailpile.nix4
-rw-r--r--nixos/modules/services/networking/morty.nix16
-rw-r--r--nixos/modules/services/networking/mosquitto.nix46
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix7
-rw-r--r--nixos/modules/services/networking/murmur.nix14
-rw-r--r--nixos/modules/services/networking/namecoind.nix4
-rw-r--r--nixos/modules/services/networking/nar-serve.nix55
-rw-r--r--nixos/modules/services/networking/nat.nix120
-rw-r--r--nixos/modules/services/networking/networkmanager.nix6
-rw-r--r--nixos/modules/services/networking/nextdns.nix4
-rw-r--r--nixos/modules/services/networking/nix-store-gcs-proxy.nix2
-rw-r--r--nixos/modules/services/networking/nomad.nix165
-rw-r--r--nixos/modules/services/networking/nsd.nix4
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix56
-rw-r--r--nixos/modules/services/networking/ntp/ntpd.nix1
-rw-r--r--nixos/modules/services/networking/owamp.nix2
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix44
-rw-r--r--nixos/modules/services/networking/pleroma.nix140
-rw-r--r--nixos/modules/services/networking/pleroma.xml132
-rw-r--r--nixos/modules/services/networking/powerdns.nix40
-rw-r--r--nixos/modules/services/networking/prayer.nix3
-rw-r--r--nixos/modules/services/networking/privoxy.nix25
-rw-r--r--nixos/modules/services/networking/prosody.nix2
-rw-r--r--nixos/modules/services/networking/quassel.nix6
-rw-r--r--nixos/modules/services/networking/radvd.nix1
-rw-r--r--nixos/modules/services/networking/resilio.nix1
-rw-r--r--nixos/modules/services/networking/sabnzbd.nix3
-rw-r--r--nixos/modules/services/networking/searx.nix214
-rw-r--r--nixos/modules/services/networking/shairport-sync.nix2
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix91
-rw-r--r--nixos/modules/services/networking/smokeping.nix30
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix18
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix9
-rw-r--r--nixos/modules/services/networking/sslh.nix2
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix2
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix20
-rw-r--r--nixos/modules/services/networking/strongswan.nix2
-rw-r--r--nixos/modules/services/networking/stunnel.nix9
-rw-r--r--nixos/modules/services/networking/supybot.nix7
-rw-r--r--nixos/modules/services/networking/tailscale.nix37
-rw-r--r--nixos/modules/services/networking/tinc.nix238
-rw-r--r--nixos/modules/services/networking/unbound.nix141
-rw-r--r--nixos/modules/services/networking/wakeonlan.nix2
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix2
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix4
-rw-r--r--nixos/modules/services/printing/cupsd.nix2
-rw-r--r--nixos/modules/services/search/elasticsearch-curator.nix1
-rw-r--r--nixos/modules/services/security/fail2ban.nix4
-rw-r--r--nixos/modules/services/security/fprintd.nix4
-rw-r--r--nixos/modules/services/security/fprot.nix3
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix5
-rw-r--r--nixos/modules/services/security/oauth2_proxy_nginx.nix2
-rw-r--r--nixos/modules/services/security/sshguard.nix6
-rw-r--r--nixos/modules/services/security/tor.nix1417
-rw-r--r--nixos/modules/services/security/usbguard.nix6
-rw-r--r--nixos/modules/services/security/vault.nix50
-rw-r--r--nixos/modules/services/system/cloud-init.nix4
-rw-r--r--nixos/modules/services/system/dbus.nix35
-rw-r--r--nixos/modules/services/torrent/deluge.nix1
-rw-r--r--nixos/modules/services/torrent/transmission.nix10
-rw-r--r--nixos/modules/services/ttys/getty.nix (renamed from nixos/modules/services/ttys/agetty.nix)49
-rw-r--r--nixos/modules/services/ttys/kmscon.nix7
-rwxr-xr-xnixos/modules/services/video/epgstation/generate31
-rw-r--r--nixos/modules/services/video/epgstation/streaming.json126
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix2
-rw-r--r--nixos/modules/services/web-apps/engelsystem.nix2
-rw-r--r--nixos/modules/services/web-apps/frab.nix222
-rw-r--r--nixos/modules/services/web-apps/galene.nix178
-rw-r--r--nixos/modules/services/web-apps/gerrit.nix2
-rw-r--r--nixos/modules/services/web-apps/grocy.nix6
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix (renamed from nixos/modules/services/web-apps/codimd.nix)121
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix77
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix2
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix9
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix692
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml205
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix564
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix1
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix2
-rw-r--r--nixos/modules/services/web-apps/moinmoin.nix7
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix91
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml17
-rw-r--r--nixos/modules/services/web-apps/plantuml-server.nix123
-rw-r--r--nixos/modules/services/web-apps/shiori.nix51
-rw-r--r--nixos/modules/services/web-apps/trilium.nix2
-rw-r--r--nixos/modules/services/web-apps/whitebophir.nix45
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix16
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix17
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix2
-rw-r--r--nixos/modules/services/web-servers/caddy.nix34
-rw-r--r--nixos/modules/services/web-servers/jboss/default.nix6
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix103
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix40
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix7
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix1
-rw-r--r--nixos/modules/services/web-servers/traefik.nix4
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix2
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix72
-rw-r--r--nixos/modules/services/x11/clight.nix30
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix1
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix92
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix21
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix22
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix1
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix1
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix185
-rw-r--r--nixos/modules/services/x11/display-managers/startx.nix12
-rw-r--r--nixos/modules/services/x11/hardware/libinput.nix97
-rw-r--r--nixos/modules/services/x11/redshift.nix11
-rw-r--r--nixos/modules/services/x11/terminal-server.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/clfswm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/evilwm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/exwm.nix18
-rw-r--r--nixos/modules/services/x11/window-managers/herbstluftwm.nix13
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix108
-rw-r--r--nixos/modules/services/x11/xserver.nix40
-rw-r--r--nixos/modules/system/activation/activation-script.nix38
-rw-r--r--nixos/modules/system/activation/top-level.nix5
-rw-r--r--nixos/modules/system/boot/binfmt.nix12
-rw-r--r--nixos/modules/system/boot/grow-partition.nix6
-rw-r--r--nixos/modules/system/boot/initrd-network.nix4
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix7
-rw-r--r--nixos/modules/system/boot/kernel.nix20
-rw-r--r--nixos/modules/system/boot/loader/generations-dir/generations-dir.nix5
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh2
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix28
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl225
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script-builder.sh1
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix5
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix9
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py6
-rw-r--r--nixos/modules/system/boot/luksroot.nix58
-rw-r--r--nixos/modules/system/boot/networkd.nix5
-rw-r--r--nixos/modules/system/boot/pbkdf2-sha512.c2
-rw-r--r--nixos/modules/system/boot/plymouth.nix2
-rw-r--r--nixos/modules/system/boot/resolved.nix7
-rw-r--r--nixos/modules/system/boot/shutdown.nix2
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh30
-rw-r--r--nixos/modules/system/boot/stage-1.nix76
-rw-r--r--nixos/modules/system/boot/stage-2.nix2
-rw-r--r--nixos/modules/system/boot/systemd-lib.nix6
-rw-r--r--nixos/modules/system/boot/systemd-unit-options.nix16
-rw-r--r--nixos/modules/system/boot/systemd.nix42
-rw-r--r--nixos/modules/system/boot/timesyncd.nix1
-rw-r--r--nixos/modules/system/boot/tmp.nix9
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix5
-rw-r--r--nixos/modules/tasks/filesystems.nix11
-rw-r--r--nixos/modules/tasks/filesystems/bcachefs.nix4
-rw-r--r--nixos/modules/tasks/filesystems/nfs.nix48
-rw-r--r--nixos/modules/tasks/filesystems/unionfs-fuse.nix6
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix155
-rw-r--r--nixos/modules/tasks/lvm.nix4
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix12
-rw-r--r--nixos/modules/tasks/network-interfaces.nix21
-rw-r--r--nixos/modules/testing/service-runner.nix4
-rw-r--r--nixos/modules/testing/test-instrumentation.nix27
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix5
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix2
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix4
-rw-r--r--nixos/modules/virtualisation/brightbox-image.nix2
-rw-r--r--nixos/modules/virtualisation/cri-o.nix5
-rw-r--r--nixos/modules/virtualisation/docker.nix7
-rw-r--r--nixos/modules/virtualisation/ec2-amis.nix21
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.nix80
-rw-r--r--nixos/modules/virtualisation/fetch-instance-ssh-keys.bash36
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix25
-rw-r--r--nixos/modules/virtualisation/google-compute-image.nix2
-rw-r--r--nixos/modules/virtualisation/hyperv-guest.nix4
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix2
-rw-r--r--nixos/modules/virtualisation/lxd.nix28
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix87
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix43
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix2
-rw-r--r--nixos/modules/virtualisation/openstack-metadata-fetcher.nix21
-rw-r--r--nixos/modules/virtualisation/podman.nix52
-rw-r--r--nixos/modules/virtualisation/qemu-guest-agent.nix7
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix34
-rw-r--r--nixos/modules/virtualisation/railcar.nix2
-rw-r--r--nixos/modules/virtualisation/vagrant-guest.nix58
-rw-r--r--nixos/modules/virtualisation/vagrant-virtualbox-image.nix60
-rw-r--r--nixos/modules/virtualisation/xen-dom0.nix9
-rw-r--r--nixos/release-combined.nix5
-rw-r--r--nixos/release-small.nix3
-rw-r--r--nixos/release.nix4
-rw-r--r--nixos/tests/3proxy.nix2
-rw-r--r--nixos/tests/acme.nix73
-rw-r--r--nixos/tests/agda.nix13
-rw-r--r--nixos/tests/all-tests.nix95
-rw-r--r--nixos/tests/ammonite.nix4
-rw-r--r--nixos/tests/atd.nix2
-rw-r--r--nixos/tests/avahi.nix18
-rw-r--r--nixos/tests/awscli.nix17
-rw-r--r--nixos/tests/babeld.nix2
-rw-r--r--nixos/tests/bat.nix12
-rw-r--r--nixos/tests/bcachefs.nix2
-rw-r--r--nixos/tests/bees.nix2
-rw-r--r--nixos/tests/bitcoind.nix10
-rw-r--r--nixos/tests/bittorrent.nix2
-rw-r--r--nixos/tests/bitwarden.nix2
-rw-r--r--nixos/tests/blockbook-frontend.nix2
-rw-r--r--nixos/tests/boot-stage1.nix2
-rw-r--r--nixos/tests/borgbackup.nix2
-rw-r--r--nixos/tests/buildbot.nix2
-rw-r--r--nixos/tests/buildkite-agents.nix2
-rw-r--r--nixos/tests/caddy.nix10
-rw-r--r--nixos/tests/cadvisor.nix8
-rw-r--r--nixos/tests/cage.nix2
-rw-r--r--nixos/tests/cagebreak.nix21
-rw-r--r--nixos/tests/cassandra.nix10
-rw-r--r--nixos/tests/ceph-multi-node.nix2
-rw-r--r--nixos/tests/ceph-single-node.nix2
-rw-r--r--nixos/tests/certmgr.nix30
-rw-r--r--nixos/tests/cfssl.nix2
-rw-r--r--nixos/tests/charliecloud.nix2
-rw-r--r--nixos/tests/chromium.nix149
-rw-r--r--nixos/tests/cifs-utils.nix12
-rw-r--r--nixos/tests/cjdns.nix2
-rw-r--r--nixos/tests/clickhouse.nix2
-rw-r--r--nixos/tests/cloud-init.nix60
-rw-r--r--nixos/tests/cockroachdb.nix2
-rw-r--r--nixos/tests/codimd.nix60
-rw-r--r--nixos/tests/common/acme/server/README.md21
-rw-r--r--nixos/tests/common/acme/server/acme.test.cert.pem19
-rw-r--r--nixos/tests/common/acme/server/acme.test.key.pem27
-rw-r--r--nixos/tests/common/acme/server/ca.cert.pem20
-rw-r--r--nixos/tests/common/acme/server/ca.key.pem27
-rw-r--r--nixos/tests/common/acme/server/default.nix7
-rw-r--r--nixos/tests/common/acme/server/generate-certs.nix29
-rw-r--r--nixos/tests/common/acme/server/snakeoil-certs.nix32
-rw-r--r--nixos/tests/containers-bridge.nix2
-rw-r--r--nixos/tests/containers-custom-pkgs.nix50
-rw-r--r--nixos/tests/containers-extra_veth.nix2
-rw-r--r--nixos/tests/containers-hosts.nix2
-rw-r--r--nixos/tests/containers-imperative.nix2
-rw-r--r--nixos/tests/containers-ip.nix2
-rw-r--r--nixos/tests/containers-macvlans.nix2
-rw-r--r--nixos/tests/containers-physical_interfaces.nix2
-rw-r--r--nixos/tests/containers-portforward.nix2
-rw-r--r--nixos/tests/containers-reloadable.nix2
-rw-r--r--nixos/tests/containers-restart_networking.nix2
-rw-r--r--nixos/tests/containers-tmpfs.nix4
-rw-r--r--nixos/tests/convos.nix4
-rw-r--r--nixos/tests/corerad.nix2
-rw-r--r--nixos/tests/couchdb.nix2
-rw-r--r--nixos/tests/cri-o.nix2
-rw-r--r--nixos/tests/custom-ca.nix161
-rw-r--r--nixos/tests/deluge.nix2
-rw-r--r--nixos/tests/dnscrypt-proxy2.nix2
-rw-r--r--nixos/tests/dnscrypt-wrapper/default.nix3
-rw-r--r--nixos/tests/docker-edge.nix6
-rw-r--r--nixos/tests/docker-registry.nix2
-rw-r--r--nixos/tests/docker-tools-cross.nix76
-rw-r--r--nixos/tests/docker-tools-overlay.nix2
-rw-r--r--nixos/tests/docker-tools.nix26
-rw-r--r--nixos/tests/docker.nix2
-rw-r--r--nixos/tests/documize.nix2
-rw-r--r--nixos/tests/dokuwiki.nix2
-rw-r--r--nixos/tests/dovecot.nix7
-rw-r--r--nixos/tests/elk.nix2
-rw-r--r--nixos/tests/emacs-daemon.nix2
-rw-r--r--nixos/tests/engelsystem.nix2
-rw-r--r--nixos/tests/enlightenment.nix2
-rw-r--r--nixos/tests/env.nix2
-rw-r--r--nixos/tests/ergo.nix2
-rw-r--r--nixos/tests/etcd-cluster.nix2
-rw-r--r--nixos/tests/etcd.nix2
-rw-r--r--nixos/tests/etesync-dav.nix21
-rw-r--r--nixos/tests/fcitx/config12
-rw-r--r--nixos/tests/fcitx/default.nix142
-rw-r--r--nixos/tests/fcitx/profile4
-rw-r--r--nixos/tests/fenics.nix2
-rw-r--r--nixos/tests/ferm.nix3
-rw-r--r--nixos/tests/firefox.nix93
-rw-r--r--nixos/tests/firejail.nix11
-rw-r--r--nixos/tests/firewall.nix2
-rw-r--r--nixos/tests/freeswitch.nix2
-rw-r--r--nixos/tests/fsck.nix2
-rw-r--r--nixos/tests/ft2-clone.nix35
-rw-r--r--nixos/tests/gerrit.nix2
-rw-r--r--nixos/tests/git/hub.nix17
-rw-r--r--nixos/tests/gitdaemon.nix2
-rw-r--r--nixos/tests/gitea.nix1
-rw-r--r--nixos/tests/gitlab.nix8
-rw-r--r--nixos/tests/gitolite-fcgiwrap.nix2
-rw-r--r--nixos/tests/gitolite.nix2
-rw-r--r--nixos/tests/glusterfs.nix4
-rw-r--r--nixos/tests/go-neb.nix4
-rw-r--r--nixos/tests/gocd-agent.nix2
-rw-r--r--nixos/tests/gocd-server.nix2
-rw-r--r--nixos/tests/google-oslogin/default.nix2
-rw-r--r--nixos/tests/gotify-server.nix2
-rw-r--r--nixos/tests/grafana.nix14
-rw-r--r--nixos/tests/grocy.nix2
-rw-r--r--nixos/tests/gvisor.nix2
-rw-r--r--nixos/tests/hadoop/hdfs.nix4
-rw-r--r--nixos/tests/hadoop/yarn.nix4
-rw-r--r--nixos/tests/haka.nix2
-rw-r--r--nixos/tests/handbrake.nix2
-rw-r--r--nixos/tests/haproxy.nix6
-rw-r--r--nixos/tests/hardened.nix4
-rw-r--r--nixos/tests/hedgedoc.nix60
-rw-r--r--nixos/tests/herbstluftwm.nix38
-rw-r--r--nixos/tests/hitch/default.nix4
-rw-r--r--nixos/tests/hledger-web.nix53
-rw-r--r--nixos/tests/hocker-fetchdocker/default.nix2
-rw-r--r--nixos/tests/home-assistant.nix2
-rw-r--r--nixos/tests/hostname.nix16
-rw-r--r--nixos/tests/hound.nix4
-rw-r--r--nixos/tests/hydra/common.nix2
-rw-r--r--nixos/tests/hydra/db-migration.nix92
-rw-r--r--nixos/tests/hydra/default.nix4
-rw-r--r--nixos/tests/i3wm.nix2
-rw-r--r--nixos/tests/icingaweb2.nix2
-rw-r--r--nixos/tests/iftop.nix2
-rw-r--r--nixos/tests/image-contents.nix51
-rw-r--r--nixos/tests/influxdb.nix2
-rw-r--r--nixos/tests/initrd-network-ssh/default.nix4
-rw-r--r--nixos/tests/initrd-network.nix2
-rw-r--r--nixos/tests/initrd-secrets.nix35
-rw-r--r--nixos/tests/installed-tests/default.nix2
-rw-r--r--nixos/tests/installed-tests/fwupd.nix2
-rw-r--r--nixos/tests/installed-tests/gsconnect.nix7
-rw-r--r--nixos/tests/installed-tests/pipewire.nix5
-rw-r--r--nixos/tests/installer.nix23
-rw-r--r--nixos/tests/ipfs.nix2
-rw-r--r--nixos/tests/ipv6.nix2
-rw-r--r--nixos/tests/jenkins.nix2
-rw-r--r--nixos/tests/jitsi-meet.nix2
-rw-r--r--nixos/tests/jq.nix10
-rw-r--r--nixos/tests/k3s.nix2
-rw-r--r--nixos/tests/kafka.nix11
-rw-r--r--nixos/tests/keepassxc.nix34
-rw-r--r--nixos/tests/kernel-latest-ath-user-regd.nix17
-rw-r--r--nixos/tests/kernel-latest.nix2
-rw-r--r--nixos/tests/kernel-lts.nix2
-rw-r--r--nixos/tests/kernel-testing.nix2
-rw-r--r--nixos/tests/keycloak.nix144
-rw-r--r--nixos/tests/knot.nix2
-rw-r--r--nixos/tests/krb5/deprecated-config.nix2
-rw-r--r--nixos/tests/krb5/example-config.nix2
-rw-r--r--nixos/tests/leaps.nix4
-rw-r--r--nixos/tests/lightdm.nix2
-rw-r--r--nixos/tests/limesurvey.nix4
-rw-r--r--nixos/tests/locate.nix62
-rw-r--r--nixos/tests/login.nix4
-rw-r--r--nixos/tests/loki.nix35
-rw-r--r--nixos/tests/lsd.nix12
-rw-r--r--nixos/tests/lxd-nftables.nix2
-rw-r--r--nixos/tests/lxd.nix2
-rw-r--r--nixos/tests/magic-wormhole-mailbox-server.nix2
-rw-r--r--nixos/tests/magnetico.nix2
-rw-r--r--nixos/tests/mailcatcher.nix2
-rw-r--r--nixos/tests/mailhog.nix24
-rw-r--r--nixos/tests/matrix-synapse.nix6
-rw-r--r--nixos/tests/mediawiki.nix2
-rw-r--r--nixos/tests/metabase.nix4
-rw-r--r--nixos/tests/minecraft-server.nix37
-rw-r--r--nixos/tests/minecraft.nix28
-rw-r--r--nixos/tests/miniflux.nix2
-rw-r--r--nixos/tests/minio.nix2
-rw-r--r--nixos/tests/misc.nix8
-rw-r--r--nixos/tests/molly-brown.nix2
-rw-r--r--nixos/tests/mongodb.nix2
-rw-r--r--nixos/tests/morty.nix10
-rw-r--r--nixos/tests/mosquitto.nix2
-rw-r--r--nixos/tests/mpd.nix10
-rw-r--r--nixos/tests/mumble.nix2
-rw-r--r--nixos/tests/munin.nix4
-rw-r--r--nixos/tests/mutable-users.nix2
-rw-r--r--nixos/tests/mxisd.nix2
-rw-r--r--nixos/tests/mysql/mariadb-galera-mariabackup.nix2
-rw-r--r--nixos/tests/mysql/mariadb-galera-rsync.nix2
-rw-r--r--nixos/tests/mysql/mysql-backup.nix2
-rw-r--r--nixos/tests/mysql/mysql-replication.nix2
-rw-r--r--nixos/tests/mysql/mysql.nix18
-rw-r--r--nixos/tests/n8n.nix25
-rw-r--r--nixos/tests/nagios.nix2
-rw-r--r--nixos/tests/nano.nix44
-rw-r--r--nixos/tests/nar-serve.nix48
-rw-r--r--nixos/tests/nat.nix2
-rw-r--r--nixos/tests/ncdns.nix2
-rw-r--r--nixos/tests/ndppd.nix2
-rw-r--r--nixos/tests/neo4j.nix2
-rw-r--r--nixos/tests/netdata.nix2
-rw-r--r--nixos/tests/networking-proxy.nix2
-rw-r--r--nixos/tests/networking.nix30
-rw-r--r--nixos/tests/nextcloud/basic.nix5
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix2
-rw-r--r--nixos/tests/nextcloud/with-postgresql-and-redis.nix2
-rw-r--r--nixos/tests/nexus.nix2
-rw-r--r--nixos/tests/nfs/kerberos.nix2
-rw-r--r--nixos/tests/nfs/simple.nix4
-rw-r--r--nixos/tests/nginx-auth.nix47
-rw-r--r--nixos/tests/nginx-sandbox.nix2
-rw-r--r--nixos/tests/nginx-sso.nix2
-rw-r--r--nixos/tests/nginx.nix2
-rw-r--r--nixos/tests/nixos-generate-config.nix15
-rw-r--r--nixos/tests/nomad.nix97
-rw-r--r--nixos/tests/novacomd.nix2
-rw-r--r--nixos/tests/nsd.nix2
-rw-r--r--nixos/tests/nzbget.nix6
-rw-r--r--nixos/tests/nzbhydra2.nix17
-rw-r--r--nixos/tests/oci-containers.nix2
-rw-r--r--nixos/tests/oh-my-zsh.nix18
-rw-r--r--nixos/tests/openarena.nix2
-rw-r--r--nixos/tests/openldap.nix147
-rw-r--r--nixos/tests/openssh.nix2
-rw-r--r--nixos/tests/opentabletdriver.nix30
-rw-r--r--nixos/tests/orangefs.nix2
-rw-r--r--nixos/tests/os-prober.nix2
-rw-r--r--nixos/tests/osrm-backend.nix4
-rw-r--r--nixos/tests/overlayfs.nix2
-rw-r--r--nixos/tests/packagekit.nix2
-rw-r--r--nixos/tests/pantheon.nix2
-rw-r--r--nixos/tests/paperless.nix6
-rw-r--r--nixos/tests/peerflix.nix4
-rw-r--r--nixos/tests/pgmanage.nix2
-rw-r--r--nixos/tests/php/fpm.nix2
-rw-r--r--nixos/tests/php/httpd.nix2
-rw-r--r--nixos/tests/php/pcre.nix2
-rw-r--r--nixos/tests/pinnwand.nix2
-rw-r--r--nixos/tests/plasma5.nix5
-rw-r--r--nixos/tests/pleroma.nix265
-rw-r--r--nixos/tests/podman.nix24
-rw-r--r--nixos/tests/postfix.nix6
-rw-r--r--nixos/tests/postgis.nix2
-rw-r--r--nixos/tests/postgresql-wal-receiver.nix16
-rw-r--r--nixos/tests/postgresql.nix2
-rw-r--r--nixos/tests/power-profiles-daemon.nix45
-rw-r--r--nixos/tests/powerdns.nix60
-rw-r--r--nixos/tests/predictable-interface-names.nix8
-rw-r--r--nixos/tests/printing.nix2
-rw-r--r--nixos/tests/privacyidea.nix2
-rw-r--r--nixos/tests/prometheus-exporters.nix215
-rw-r--r--nixos/tests/prometheus.nix7
-rw-r--r--nixos/tests/proxy.nix2
-rw-r--r--nixos/tests/pt2-clone.nix2
-rw-r--r--nixos/tests/quagga.nix2
-rw-r--r--nixos/tests/quorum.nix4
-rw-r--r--nixos/tests/rabbitmq.nix2
-rw-r--r--nixos/tests/redis.nix29
-rw-r--r--nixos/tests/resolv.nix2
-rw-r--r--nixos/tests/restic.nix2
-rw-r--r--nixos/tests/riak.nix2
-rw-r--r--nixos/tests/robustirc-bridge.nix2
-rw-r--r--nixos/tests/roundcube.nix2
-rw-r--r--nixos/tests/rspamd.nix60
-rw-r--r--nixos/tests/rsyncd.nix39
-rw-r--r--nixos/tests/rsyslogd.nix4
-rw-r--r--nixos/tests/samba-wsdd.nix44
-rw-r--r--nixos/tests/samba.nix2
-rw-r--r--nixos/tests/sanoid.nix11
-rw-r--r--nixos/tests/sbt-extras.nix16
-rw-r--r--nixos/tests/sbt.nix18
-rw-r--r--nixos/tests/sddm.nix2
-rw-r--r--nixos/tests/searx.nix114
-rw-r--r--nixos/tests/service-runner.nix4
-rw-r--r--nixos/tests/shadow.nix116
-rw-r--r--nixos/tests/signal-desktop.nix2
-rw-r--r--nixos/tests/simple.nix2
-rw-r--r--nixos/tests/slurm.nix16
-rw-r--r--nixos/tests/smokeping.nix3
-rw-r--r--nixos/tests/snapcast.nix21
-rw-r--r--nixos/tests/snapper.nix2
-rw-r--r--nixos/tests/sogo.nix2
-rw-r--r--nixos/tests/solr.nix2
-rw-r--r--nixos/tests/spacecookie.nix4
-rw-r--r--nixos/tests/spike.nix4
-rw-r--r--nixos/tests/sslh.nix2
-rw-r--r--nixos/tests/sssd-ldap.nix76
-rw-r--r--nixos/tests/sssd.nix2
-rw-r--r--nixos/tests/strongswan-swanctl.nix2
-rw-r--r--nixos/tests/sudo.nix2
-rw-r--r--nixos/tests/switch-test.nix2
-rw-r--r--nixos/tests/sympa.nix2
-rw-r--r--nixos/tests/syncthing-init.nix2
-rw-r--r--nixos/tests/syncthing-relay.nix4
-rw-r--r--nixos/tests/syncthing.nix6
-rw-r--r--nixos/tests/systemd-analyze.nix2
-rw-r--r--nixos/tests/systemd-boot.nix6
-rw-r--r--nixos/tests/systemd-journal.nix22
-rw-r--r--nixos/tests/systemd-networkd-dhcpserver.nix2
-rw-r--r--nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix2
-rw-r--r--nixos/tests/systemd-networkd-vrf.nix4
-rw-r--r--nixos/tests/systemd-networkd.nix2
-rw-r--r--nixos/tests/systemd.nix39
-rw-r--r--nixos/tests/teeworlds.nix2
-rw-r--r--nixos/tests/telegraf.nix7
-rw-r--r--nixos/tests/tinc/default.nix139
-rw-r--r--nixos/tests/tinc/snakeoil-keys.nix157
-rw-r--r--nixos/tests/tor.nix2
-rw-r--r--nixos/tests/trac.nix4
-rw-r--r--nixos/tests/traefik.nix2
-rw-r--r--nixos/tests/transmission.nix2
-rw-r--r--nixos/tests/trezord.nix4
-rw-r--r--nixos/tests/trickster.nix10
-rw-r--r--nixos/tests/tuptime.nix2
-rw-r--r--nixos/tests/ucg.nix18
-rw-r--r--nixos/tests/udisks2.nix2
-rw-r--r--nixos/tests/unbound.nix278
-rw-r--r--nixos/tests/upnp.nix4
-rw-r--r--nixos/tests/usbguard.nix62
-rw-r--r--nixos/tests/uwsgi.nix69
-rw-r--r--nixos/tests/vault-postgresql.nix70
-rw-r--r--nixos/tests/vault.nix3
-rw-r--r--nixos/tests/vector.nix37
-rw-r--r--nixos/tests/victoriametrics.nix8
-rw-r--r--nixos/tests/virtualbox.nix8
-rw-r--r--nixos/tests/vscodium.nix47
-rw-r--r--nixos/tests/wasabibackend.nix2
-rw-r--r--nixos/tests/web-servers/unit-php.nix4
-rw-r--r--nixos/tests/wireguard/basic.nix2
-rw-r--r--nixos/tests/wireguard/generated.nix2
-rw-r--r--nixos/tests/wireguard/namespaces.nix2
-rw-r--r--nixos/tests/wireguard/wg-quick.nix2
-rw-r--r--nixos/tests/wordpress.nix4
-rw-r--r--nixos/tests/xautolock.nix2
-rw-r--r--nixos/tests/xmonad.nix20
-rw-r--r--nixos/tests/xmpp/ejabberd.nix2
-rw-r--r--nixos/tests/xmpp/prosody.nix2
-rw-r--r--nixos/tests/xmpp/xmpp-sendmessage.nix26
-rw-r--r--nixos/tests/xrdp.nix2
-rw-r--r--nixos/tests/xss-lock.nix2
-rw-r--r--nixos/tests/xterm.nix23
-rw-r--r--nixos/tests/yabar.nix2
-rw-r--r--nixos/tests/yggdrasil.nix2
-rw-r--r--nixos/tests/yq.nix12
-rw-r--r--nixos/tests/zfs.nix39
-rw-r--r--nixos/tests/zookeeper.nix18
-rw-r--r--nixos/tests/zsh-history.nix4
858 files changed, 20682 insertions, 7032 deletions
diff --git a/nixos/default.nix b/nixos/default.nix
index 45da78e9261cc..c11872f1441ab 100644
--- a/nixos/default.nix
+++ b/nixos/default.nix
@@ -22,6 +22,11 @@ let
       [ configuration
         ./modules/virtualisation/qemu-vm.nix
         { virtualisation.useBootLoader = true; }
+        ({ config, ... }: {
+          virtualisation.useEFIBoot =
+            config.boot.loader.systemd-boot.enable ||
+            config.boot.loader.efi.canTouchEfiVariables;
+        })
       ];
   }).config;
 
diff --git a/nixos/doc/manual/README b/nixos/doc/manual/README
index 587f6275197ae..120c127d7af20 100644
--- a/nixos/doc/manual/README
+++ b/nixos/doc/manual/README
@@ -1,12 +1,3 @@
-To build the manual, you need Nix installed on your system (no need
-for NixOS). To install Nix, follow the instructions at
+Moved to: ./contributing-to-this-manual.xml. Link:
 
-    https://nixos.org/nix/download.html
-
-When you have Nix on your system, in the root directory of the project
-(i.e., `nixpkgs`), run:
-
-    nix-build nixos/release.nix -A manual.x86_64-linux
-
-When this command successfully finishes, it will tell you where the
-manual got generated.
+https://nixos.org/manual/nixos/unstable/#chap-contributing
diff --git a/nixos/doc/manual/administration/service-mgmt.xml b/nixos/doc/manual/administration/service-mgmt.xml
index 1c5d48a5bcf07..863b0d47f6c7b 100644
--- a/nixos/doc/manual/administration/service-mgmt.xml
+++ b/nixos/doc/manual/administration/service-mgmt.xml
@@ -6,7 +6,7 @@
  <title>Service Management</title>
  <para>
   In NixOS, all system services are started and monitored using the systemd
-  program. Systemd is the “init” process of the system (i.e. PID 1), the
+  program. systemd is the “init” process of the system (i.e. PID 1), the
   parent of all other processes. It manages a set of so-called “units”,
   which can be things like system services (programs), but also mount points,
   swap files, devices, targets (groups of units) and more. Units can have
@@ -16,10 +16,17 @@
   dependencies of this unit cause all system services to be started, file
   systems to be mounted, swap files to be activated, and so on.
  </para>
- <para>
-  The command <command>systemctl</command> is the main way to interact with
-  <command>systemd</command>. Without any arguments, it shows the status of
-  active units:
+ <section xml:id="sect-nixos-systemd-general">
+  <title>Interacting with a running systemd</title>
+   <para>
+    The command <command>systemctl</command> is the main way to interact with
+    <command>systemd</command>. The following paragraphs demonstrate ways to
+    interact with any OS running systemd as init system. NixOS is of no
+    exception. The <link xlink:href="#sect-nixos-systemd-nixos">next section
+    </link> explains NixOS specific things worth knowing.
+   </para>
+   <para>
+    Without any arguments, <literal>systmctl</literal> the status of active units:
 <screen>
 <prompt>$ </prompt>systemctl
 -.mount          loaded active mounted   /
@@ -28,10 +35,10 @@ sshd.service     loaded active running   SSH Daemon
 graphical.target loaded active active    Graphical Interface
 <replaceable>...</replaceable>
 </screen>
- </para>
- <para>
-  You can ask for detailed status information about a unit, for instance, the
-  PostgreSQL database service:
+  </para>
+  <para>
+   You can ask for detailed status information about a unit, for instance, the
+   PostgreSQL database service:
 <screen>
 <prompt>$ </prompt>systemctl status postgresql.service
 postgresql.service - PostgreSQL Server
@@ -62,11 +69,72 @@ Jan 07 15:55:57 hagbard systemd[1]: Started PostgreSQL Server.
 <prompt># </prompt>systemctl start postgresql.service
 <prompt># </prompt>systemctl restart postgresql.service
 </screen>
-  These operations are synchronous: they wait until the service has finished
-  starting or stopping (or has failed). Starting a unit will cause the
-  dependencies of that unit to be started as well (if necessary).
- </para>
-<!-- - cgroups: each service and user session is a cgroup
+   These operations are synchronous: they wait until the service has finished
+   starting or stopping (or has failed). Starting a unit will cause the
+   dependencies of that unit to be started as well (if necessary).
+  </para>
+  <!-- TODO: document cgroups, draft:
+   each service and user session is a cgroup
 
-- cgroup resource management -->
+   - cgroup resource management -->
+ </section>
+ <section xml:id="sect-nixos-systemd-nixos">
+  <title>systemd in NixOS</title>
+  <para>
+   Packages in Nixpkgs sometimes provide systemd units with them, usually in
+   e.g <literal>#pkg-out#/lib/systemd/</literal>. Putting such a package in
+   <literal>environment.systemPackages</literal> doesn't make the service
+   available to users or the system.
+  </para>
+  <para>
+   In order to enable a systemd <emphasis>system</emphasis> service with
+   provided upstream package, use (e.g):
+<programlisting>
+<xref linkend="opt-systemd.packages"/> = [ pkgs.packagekit ];
+</programlisting>
+  </para>
+  <para>
+   Usually NixOS modules written by the community do the above, plus take care of
+   other details. If a module was written for a service you are interested in,
+   you'd probably need only to use
+   <literal>services.#name#.enable = true;</literal>. These services are defined
+   in Nixpkgs'
+   <link xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/modules">
+   <literal>nixos/modules/</literal> directory </link>. In case the service is
+   simple enough, the above method should work, and start the service on boot.
+  </para>
+  <para>
+   <emphasis>User</emphasis> systemd services on the other hand, should be
+   treated differently. Given a package that has a systemd unit file at
+   <literal>#pkg-out#/lib/systemd/user/</literal>, using
+   <xref linkend="opt-systemd.packages"/> will make you able to start the service via
+   <literal>systemctl --user start</literal>, but it won't start automatically on login.
+   <!-- TODO: Document why systemd.packages doesn't work for user services or fix this.
+   https://github.com/NixOS/nixpkgs/blob/2cd6594a8710a801038af2b72348658f732ce84a/nixos/modules/system/boot/systemd-lib.nix#L177-L198
+
+   This has been talked over at https://discourse.nixos.org/t/how-to-enable-upstream-systemd-user-services-declaratively/7649/5
+   -->
+   However, You can imperatively enable it by adding the package's attribute to
+   <link linkend="opt-environment.systemPackages">
+   <literal>systemd.packages</literal></link> and then do this (e.g):
+<screen>
+<prompt>$ </prompt>mkdir -p ~/.config/systemd/user/default.target.wants
+<prompt>$ </prompt>ln -s /run/current-system/sw/lib/systemd/user/syncthing.service ~/.config/systemd/user/default.target.wants/
+<prompt>$ </prompt>systemctl --user daemon-reload
+<prompt>$ </prompt>systemctl --user enable syncthing.service
+</screen>
+   If you are interested in a timer file, use <literal>timers.target.wants</literal>
+   instead of <literal>default.target.wants</literal> in the 1st and 2nd command.
+  </para>
+  <para>
+   Using <literal>systemctl --user enable syncthing.service</literal> instead of
+   the above, will work, but it'll use the absolute path of
+   <literal>syncthing.service</literal> for the symlink, and this path is in
+   <literal>/nix/store/.../lib/systemd/user/</literal>. Hence
+   <link xlink:href="#sec-nix-gc">garbage collection</link> will remove that file
+   and you will wind up with a broken symlink in your systemd configuration, which
+   in turn will not make the service / timer start on login.
+  </para>
+ </section>
 </chapter>
+
diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml
index 6eb8f50bacac6..6949189b8883c 100644
--- a/nixos/doc/manual/configuration/configuration.xml
+++ b/nixos/doc/manual/configuration/configuration.xml
@@ -18,10 +18,12 @@
  <xi:include href="user-mgmt.xml" />
  <xi:include href="file-systems.xml" />
  <xi:include href="x-windows.xml" />
+ <xi:include href="wayland.xml" />
  <xi:include href="gpu-accel.xml" />
  <xi:include href="xfce.xml" />
  <xi:include href="networking.xml" />
  <xi:include href="linux-kernel.xml" />
+ <xi:include href="subversion.xml" />
  <xi:include href="../generated/modules.xml" xpointer="xpointer(//section[@id='modules']/*)" />
  <xi:include href="profiles.xml" />
  <xi:include href="kubernetes.xml" />
diff --git a/nixos/doc/manual/configuration/gpu-accel.xml b/nixos/doc/manual/configuration/gpu-accel.xml
index 95ee13f4796e8..3695a5ff445a4 100644
--- a/nixos/doc/manual/configuration/gpu-accel.xml
+++ b/nixos/doc/manual/configuration/gpu-accel.xml
@@ -65,16 +65,16 @@ Platform Vendor      Advanced Micro Devices, Inc.</screen>
       <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>
+       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>
 
@@ -100,9 +100,9 @@ Platform Vendor      Advanced Micro Devices, Inc.</screen>
        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>
+      <programlisting><xref linkend="opt-hardware.opengl.extraPackages"/> = [
+        intel-compute-runtime
+      ];</programlisting>
 
       </para>
     </section>
@@ -173,31 +173,31 @@ GPU1:
       <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"/> = [
-  pkgs.<package>amdvlk</package>
-];
-
-# To enable Vulkan support for 32-bit applications, also add:
-<xref linkend="opt-hardware.opengl.extraPackages32"/> = [
-  pkgs.driversi686Linux.<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>
+       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"/> = [
+         pkgs.<package>amdvlk</package>
+       ];
+
+       # To enable Vulkan support for 32-bit applications, also add:
+       <xref linkend="opt-hardware.opengl.extraPackages32"/> = [
+         pkgs.driversi686Linux.<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>
diff --git a/nixos/doc/manual/configuration/linux-kernel.xml b/nixos/doc/manual/configuration/linux-kernel.xml
index dbdcc94149548..529ac1b1cd468 100644
--- a/nixos/doc/manual/configuration/linux-kernel.xml
+++ b/nixos/doc/manual/configuration/linux-kernel.xml
@@ -87,7 +87,7 @@ nixpkgs.config.packageOverrides = pkgs:
    You can edit the config with this snippet (by default <command>make
    menuconfig</command> won't work out of the box on nixos):
 <screen><![CDATA[
-      nix-shell -E 'with import <nixpkgs> {}; kernelToOverride.overrideAttrs (o: {nativeBuildInputs=o.nativeBuildInputs ++ [ pkgconfig ncurses ];})'
+      nix-shell -E 'with import <nixpkgs> {}; kernelToOverride.overrideAttrs (o: {nativeBuildInputs=o.nativeBuildInputs ++ [ pkg-config ncurses ];})'
   ]]></screen>
    or you can let nixpkgs generate the configuration. Nixpkgs generates it via
    answering the interactive kernel utility <command>make config</command>. The
diff --git a/nixos/doc/manual/configuration/networking.xml b/nixos/doc/manual/configuration/networking.xml
index 02cf811e0bd3d..8369e9c9c852b 100644
--- a/nixos/doc/manual/configuration/networking.xml
+++ b/nixos/doc/manual/configuration/networking.xml
@@ -15,5 +15,6 @@
  <xi:include href="firewall.xml" />
  <xi:include href="wireless.xml" />
  <xi:include href="ad-hoc-network-config.xml" />
+ <xi:include href="renaming-interfaces.xml" />
 <!-- TODO: OpenVPN, NAT -->
 </chapter>
diff --git a/nixos/doc/manual/configuration/profiles/hardened.xml b/nixos/doc/manual/configuration/profiles/hardened.xml
index dc83fc837e2a3..4a51754cc7ae2 100644
--- a/nixos/doc/manual/configuration/profiles/hardened.xml
+++ b/nixos/doc/manual/configuration/profiles/hardened.xml
@@ -7,7 +7,7 @@
 
  <para>
   A profile with most (vanilla) hardening options enabled by default,
-  potentially at the cost of features and performance.
+  potentially at the cost of stability, features and performance.
  </para>
 
  <para>
@@ -21,4 +21,12 @@
    xlink:href="https://github.com/nixos/nixpkgs/tree/master/nixos/modules/profiles/hardened.nix">
   profile source</literal> for further detail on which settings are altered.
  </para>
+ <warning>
+   <para>
+     This profile enables options that are known to affect system
+     stability. If you experience any stability issues when using the
+     profile, try disabling it. If you report an issue and use this
+     profile, always mention that you do.
+   </para>
+ </warning>
 </section>
diff --git a/nixos/doc/manual/configuration/profiles/qemu-guest.xml b/nixos/doc/manual/configuration/profiles/qemu-guest.xml
index 5d055c45d2d83..3ed97b94b5100 100644
--- a/nixos/doc/manual/configuration/profiles/qemu-guest.xml
+++ b/nixos/doc/manual/configuration/profiles/qemu-guest.xml
@@ -11,8 +11,7 @@
  </para>
 
  <para>
-  It makes virtio modules available on the initrd, sets the system time from
-  the hardware clock to work around a bug in qemu-kvm, and
-  <link linkend="opt-security.rngd.enable">enables rngd</link>.
+  It makes virtio modules available on the initrd and sets the system time from
+  the hardware clock to work around a bug in qemu-kvm.
  </para>
 </section>
diff --git a/nixos/doc/manual/configuration/renaming-interfaces.xml b/nixos/doc/manual/configuration/renaming-interfaces.xml
new file mode 100644
index 0000000000000..d760bb3a4dace
--- /dev/null
+++ b/nixos/doc/manual/configuration/renaming-interfaces.xml
@@ -0,0 +1,67 @@
+<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-rename-ifs">
+ <title>Renaming network interfaces</title>
+
+ <para>
+  NixOS uses the udev
+  <link xlink:href="https://systemd.io/PREDICTABLE_INTERFACE_NAMES/">predictable naming scheme</link>
+  to assign names to network interfaces. This means that by default
+  cards are not given the traditional names like
+  <literal>eth0</literal> or <literal>eth1</literal>, whose order can
+  change unpredictably across reboots. Instead, relying on physical
+  locations and firmware information, the scheme produces names like
+  <literal>ens1</literal>, <literal>enp2s0</literal>, etc.
+ </para>
+
+ <para>
+  These names are predictable but less memorable and not necessarily
+  stable: for example installing new hardware or changing firmware
+  settings can result in a
+  <link xlink:href="https://github.com/systemd/systemd/issues/3715#issue-165347602">name change</link>.
+  If this is undesirable, for example if you have a single ethernet
+  card, you can revert to the traditional scheme by setting
+  <xref linkend="opt-networking.usePredictableInterfaceNames"/> to
+  <literal>false</literal>.
+ </para>
+
+ <section xml:id="sec-custom-ifnames">
+  <title>Assigning custom names</title>
+  <para>
+   In case there are multiple interfaces of the same type, it’s better to
+   assign custom names based on the device hardware address. For
+   example, we assign the name <literal>wan</literal> to the interface
+   with MAC address <literal>52:54:00:12:01:01</literal> using a
+   netword link unit:
+  </para>
+  <programlisting>
+ <link linkend="opt-systemd.network.links">systemd.network.links."10-wan"</link> = {
+   matchConfig.MACAddress = "52:54:00:12:01:01";
+   linkConfig.Name = "wan";
+ };
+  </programlisting>
+  <para>
+   Note that links are directly read by udev, <emphasis>not networkd</emphasis>,
+   and will work even if networkd is disabled.
+  </para>
+  <para>
+   Alternatively, we can use a plain old udev rule:
+  </para>
+  <programlisting>
+ <link linkend="opt-services.udev.initrdRules">services.udev.initrdRules</link> = ''
+  SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", \
+  ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="wan"
+ '';
+  </programlisting>
+
+  <warning><para>
+   The rule must be installed in the initrd using
+   <literal>services.udev.initrdRules</literal>, not the usual
+   <literal>services.udev.extraRules</literal> option. This is to avoid race
+   conditions with other programs controlling the interface.
+  </para></warning>
+ </section>
+
+</section>
diff --git a/nixos/doc/manual/configuration/subversion.xml b/nixos/doc/manual/configuration/subversion.xml
new file mode 100644
index 0000000000000..940d63cc4e6de
--- /dev/null
+++ b/nixos/doc/manual/configuration/subversion.xml
@@ -0,0 +1,140 @@
+<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-subversion">
+  <title>Subversion</title>
+
+ <para>
+  <link xlink:href="https://subversion.apache.org/">Subversion</link>
+  is a centralized version-control system.  It can use a <link
+  xlink:href="http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.choosing">variety
+  of protocols</link> for communication between client and server.
+ </para>
+ <section xml:id="module-services-subversion-apache-httpd">
+  <title>Subversion inside Apache HTTP</title>
+
+   <para>
+   This section focuses on configuring a web-based server on top of
+   the Apache HTTP server, which uses
+   <link xlink:href="http://www.webdav.org/">WebDAV</link>/<link
+   xlink:href="http://www.webdav.org/deltav/WWW10/deltav-intro.htm">DeltaV</link>
+   for communication.
+   </para>
+
+   <para>For more information on the general setup, please refer to
+   the <link
+   xlink:href="http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.httpd">the
+   appropriate section of the Subversion book</link>.
+   </para>
+
+   <para>To configure, include in
+   <literal>/etc/nixos/configuration.nix</literal> code to activate
+   Apache HTTP, setting <xref linkend="opt-services.httpd.adminAddr" />
+   appropriately:
+   </para>
+
+    <para>
+<programlisting>
+  services.httpd.enable = true;
+  services.httpd.adminAddr = ...;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+</programlisting>
+    </para>
+
+    <para>For a simple Subversion server with basic authentication,
+    configure the Subversion module for Apache as follows, setting
+    <literal>hostName</literal> and <literal>documentRoot</literal>
+    appropriately, and <literal>SVNParentPath</literal> to the parent
+    directory of the repositories,
+    <literal>AuthzSVNAccessFile</literal> to the location of the
+    <code>.authz</code> file describing access permission, and
+    <literal>AuthUserFile</literal> to the password file.
+    </para>
+    <para>
+<programlisting>
+services.httpd.extraModules = [
+    # note that order is *super* important here
+    { name = "dav_svn"; path = "${pkgs.apacheHttpdPackages.subversion}/modules/mod_dav_svn.so"; }
+    { name = "authz_svn"; path = "${pkgs.apacheHttpdPackages.subversion}/modules/mod_authz_svn.so"; }
+  ];
+  services.httpd.virtualHosts = {
+    "svn" = {
+       hostName = HOSTNAME;
+       documentRoot = DOCUMENTROOT;
+       locations."/svn".extraConfig = ''
+           DAV svn
+           SVNParentPath REPO_PARENT
+           AuthzSVNAccessFile ACCESS_FILE
+           AuthName "SVN Repositories"
+           AuthType Basic
+           AuthUserFile PASSWORD_FILE
+           Require valid-user
+      '';
+    }
+</programlisting>
+    </para>
+
+    <para>
+    The key <code>"svn"</code> is just a symbolic name identifying the
+    virtual host.  The <code>"/svn"</code> in
+    <code>locations."/svn".extraConfig</code> is the path underneath
+    which the repositories will be served.
+    </para>
+
+    <para><link
+              xlink:href="https://wiki.archlinux.org/index.php/Subversion">This
+    page</link> explains how to set up the Subversion configuration
+    itself.  This boils down to the following:
+    </para>
+    <para>
+      Underneath <literal>REPO_PARENT</literal> repositories can be set up
+      as follows:
+    </para>
+    <para>
+<screen>
+<prompt>$ </prompt> svn create REPO_NAME
+</screen>
+    </para>
+    <para>Repository files need to be accessible by
+    <literal>wwwrun</literal>:
+    </para>
+    <para>
+<screen>
+<prompt>$ </prompt> chown -R wwwrun:wwwrun REPO_PARENT
+</screen>
+    </para>
+    <para>
+      The password file <literal>PASSWORD_FILE</literal> can be created as follows:
+    </para>
+    <para>
+<screen>
+<prompt>$ </prompt> htpasswd -cs PASSWORD_FILE USER_NAME
+</screen>
+    </para>
+    <para>
+    Additional users can be set up similarly, omitting the
+    <code>c</code> flag:
+    </para>
+    <para>
+<screen>
+<prompt>$ </prompt> htpasswd -s PASSWORD_FILE USER_NAME
+</screen>
+    </para>
+    <para>
+      The file describing access permissions
+      <literal>ACCESS_FILE</literal> will look something like
+      the following:
+    </para>
+    <para>
+<programlisting>
+[/]
+* = r
+
+[REPO_NAME:/]
+USER_NAME = rw
+</programlisting>
+    </para>
+    <para>The Subversion repositories will be accessible as <code>http://HOSTNAME/svn/REPO_NAME</code>.</para>
+ </section>
+</chapter>
diff --git a/nixos/doc/manual/configuration/user-mgmt.xml b/nixos/doc/manual/configuration/user-mgmt.xml
index cbec83814c9a3..e83e7b75ef542 100644
--- a/nixos/doc/manual/configuration/user-mgmt.xml
+++ b/nixos/doc/manual/configuration/user-mgmt.xml
@@ -38,7 +38,7 @@
   assigned by setting the user's
   <link linkend="opt-users.users._name_.hashedPassword">hashedPassword</link>
   option. A hashed password can be generated using <command>mkpasswd -m
-  sha-512</command> after installing the <literal>mkpasswd</literal> package.
+  sha-512</command>.
  </para>
  <para>
   A user ID (uid) is assigned automatically. You can also specify a uid
diff --git a/nixos/doc/manual/configuration/wayland.xml b/nixos/doc/manual/configuration/wayland.xml
new file mode 100644
index 0000000000000..231c219cf0811
--- /dev/null
+++ b/nixos/doc/manual/configuration/wayland.xml
@@ -0,0 +1,23 @@
+<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-wayland">
+ <title>Wayland</title>
+
+ <para>
+  While X11 (see <xref linkend="sec-x11"/>) is still the primary display
+  technology on NixOS, Wayland support is steadily improving.
+  Where X11 separates the X Server and the window manager, on Wayland those
+  are combined: a Wayland Compositor is like an X11 window manager, but also
+  embeds the Wayland 'Server' functionality. This means it is sufficient to
+  install a Wayland Compositor such as <package>sway</package> without
+  separately enabling a Wayland server:
+<programlisting>
+<xref linkend="opt-programs.sway.enable"/> = true;
+</programlisting>
+  This installs the <package>sway</package> compositor along with some
+  essential utilities. Now you can start <package>sway</package> from the TTY
+  console.
+ </para>
+</chapter>
diff --git a/nixos/doc/manual/configuration/x-windows.xml b/nixos/doc/manual/configuration/x-windows.xml
index b33f6cf82b527..dd879702d7dc0 100644
--- a/nixos/doc/manual/configuration/x-windows.xml
+++ b/nixos/doc/manual/configuration/x-windows.xml
@@ -186,7 +186,7 @@
    The driver has many options (see <xref linkend="ch-options"/>). For
    instance, the following disables tap-to-click behavior:
 <programlisting>
-<xref linkend="opt-services.xserver.libinput.tapping"/> = false;
+<xref linkend="opt-services.xserver.libinput.touchpad.tapping"/> = false;
 </programlisting>
    Note: the use of <literal>services.xserver.synaptics</literal> is deprecated
    since NixOS 17.09.
diff --git a/nixos/doc/manual/contributing-to-this-manual.xml b/nixos/doc/manual/contributing-to-this-manual.xml
new file mode 100644
index 0000000000000..137e04bb313b3
--- /dev/null
+++ b/nixos/doc/manual/contributing-to-this-manual.xml
@@ -0,0 +1,22 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xml:id="chap-contributing">
+ <title>Contributing to this manual</title>
+ <para>
+  The DocBook sources of NixOS' manual are in the <filename
+xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/doc/manual">
+nixos/doc/manual</filename> subdirectory of the <link
+xlink:href="https://github.com/NixOS/nixpkgs">Nixpkgs</link> repository.
+ </para>
+ <para>
+  You can quickly check your edits with the following:
+ </para>
+<screen>
+<prompt>$ </prompt>cd /path/to/nixpkgs
+<prompt>$ </prompt>nix-build nixos/release.nix -A manual.x86_64-linux
+</screen>
+ <para>
+  If the build succeeds, the manual will be in
+  <filename>./result/share/doc/nixos/index.html</filename>.
+ </para>
+</chapter>
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index 6ca75f869f45c..af7a2e08220c1 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -63,6 +63,7 @@ let
     "--stringparam html.script './highlightjs/highlight.pack.js ./highlightjs/loader.js'"
     "--param xref.with.number.and.title 1"
     "--param toc.section.depth 0"
+    "--param generate.consistent.ids 1"
     "--stringparam admon.style ''"
     "--stringparam callout.graphics.extension .svg"
     "--stringparam current.docid manual"
diff --git a/nixos/doc/manual/development/meta-attributes.xml b/nixos/doc/manual/development/meta-attributes.xml
index c626ef30e9d50..c40be0a50c36d 100644
--- a/nixos/doc/manual/development/meta-attributes.xml
+++ b/nixos/doc/manual/development/meta-attributes.xml
@@ -57,7 +57,7 @@
       linkend="ch-configuration"/>. Changes to a module documentation
     have to be checked to not break building the NixOS manual:
    </para>
-<screen><prompt>$ </prompt>nix-build nixos/release.nix -A manual</screen>
+<screen><prompt>$ </prompt>nix-build nixos/release.nix -A manual.x86_64-linux</screen>
   </callout>
  </calloutlist>
 </section>
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.xml b/nixos/doc/manual/development/running-nixos-tests-interactively.xml
index a11a9382764d2..a6044d5f89e85 100644
--- a/nixos/doc/manual/development/running-nixos-tests-interactively.xml
+++ b/nixos/doc/manual/development/running-nixos-tests-interactively.xml
@@ -9,7 +9,7 @@
   The test itself can be run interactively. This is particularly useful when
   developing or debugging a test:
 <screen>
-<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driver
+<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driverInteractive
 <prompt>$ </prompt>./result/bin/nixos-test-driver
 starting VDE switch for network 1
 <prompt>&gt;</prompt>
@@ -30,7 +30,7 @@ starting VDE switch for network 1
  <para>
   To just start and experiment with the VMs, run:
 <screen>
-<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driver
+<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driverInteractive
 <prompt>$ </prompt>./result/bin/nixos-run-vms
 </screen>
   The script <command>nixos-run-vms</command> starts the virtual machines
diff --git a/nixos/doc/manual/installation/installing-from-other-distro.xml b/nixos/doc/manual/installation/installing-from-other-distro.xml
index f10a7d6588798..43f69b923d14d 100644
--- a/nixos/doc/manual/installation/installing-from-other-distro.xml
+++ b/nixos/doc/manual/installation/installing-from-other-distro.xml
@@ -161,6 +161,13 @@ nixpkgs https://nixos.org/channels/nixpkgs-unstable</screen>
      existing systems without the help of a rescue USB drive or similar.
     </para>
    </warning>
+   <note>
+    <para>
+     On some distributions there are separate PATHS for programs intended only for root.
+     In order for the installation to succeed, you might have to use <literal>PATH="$PATH:/usr/sbin:/sbin"</literal>
+     in the following command.
+    </para>
+   </note>
 <screen><prompt>$ </prompt>sudo PATH="$PATH" NIX_PATH="$NIX_PATH" `which nixos-install` --root /mnt</screen>
    <para>
     Again, please refer to the <literal>nixos-install</literal> step in
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.xml b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
index 1cffeed480790..019e5098a8e27 100644
--- a/nixos/doc/manual/installation/installing-virtualbox-guest.xml
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
@@ -83,17 +83,12 @@
   VirtualBox settings (Machine / Settings / Shared Folders, then click on the
   "Add" icon). Add the following to the
   <literal>/etc/nixos/configuration.nix</literal> to auto-mount them. If you do
-  not add <literal>"nofail"</literal>, the system will no boot properly. The
-  same goes for disabling <literal>rngd</literal> which is normally used to get
-  randomness but this does not work in virtual machines.
+  not add <literal>"nofail"</literal>, the system will not boot properly.
  </para>
 
 <programlisting>
 { config, pkgs, ...} :
 {
-  security.rngd.enable = false; // otherwise vm will not boot
-  ...
-
   fileSystems."/virtualboxshare" = {
     fsType = "vboxsf";
     device = "nameofthesharedfolder";
diff --git a/nixos/doc/manual/installation/installing.xml b/nixos/doc/manual/installation/installing.xml
index 6df1d83034838..bedeb7ccfa8e5 100644
--- a/nixos/doc/manual/installation/installing.xml
+++ b/nixos/doc/manual/installation/installing.xml
@@ -480,13 +480,8 @@ Retype new UNIX password: ***</screen>
 <prompt>$ </prompt>passwd eelco</screen>
     </para>
     <para>
-     You may also want to install some software. For instance,
-<screen>
-<prompt>$ </prompt>nix-env -qaP \*</screen>
-     shows what packages are available, and
-<screen>
-<prompt>$ </prompt>nix-env -f '&lt;nixpkgs&gt;' -iA w3m</screen>
-     installs the <literal>w3m</literal> browser.
+     You may also want to install some software. This will be covered
+     in <xref linkend="sec-package-management" />.
     </para>
    </listitem>
   </orderedlist>
diff --git a/nixos/doc/manual/installation/upgrading.xml b/nixos/doc/manual/installation/upgrading.xml
index 08780051d5f60..15ba5db9a37be 100644
--- a/nixos/doc/manual/installation/upgrading.xml
+++ b/nixos/doc/manual/installation/upgrading.xml
@@ -14,7 +14,7 @@
     <para>
      <emphasis>Stable channels</emphasis>, such as
      <literal
-    xlink:href="https://nixos.org/channels/nixos-20.03">nixos-20.03</literal>.
+    xlink:href="https://nixos.org/channels/nixos-20.09">nixos-20.09</literal>.
      These only get conservative bug fixes and package upgrades. For instance,
      a channel update may cause the Linux kernel on your system to be upgraded
      from 4.19.34 to 4.19.38 (a minor bug fix), but not from
@@ -38,7 +38,7 @@
     <para>
      <emphasis>Small channels</emphasis>, such as
      <literal
-    xlink:href="https://nixos.org/channels/nixos-20.03-small">nixos-20.03-small</literal>
+    xlink:href="https://nixos.org/channels/nixos-20.09-small">nixos-20.09-small</literal>
      or
      <literal
     xlink:href="https://nixos.org/channels/nixos-unstable-small">nixos-unstable-small</literal>.
@@ -63,8 +63,8 @@
  <para>
   When you first install NixOS, you’re automatically subscribed to the NixOS
   channel that corresponds to your installation source. For instance, if you
-  installed from a 20.03 ISO, you will be subscribed to the
-  <literal>nixos-20.03</literal> channel. To see which NixOS channel you’re
+  installed from a 20.09 ISO, you will be subscribed to the
+  <literal>nixos-20.09</literal> channel. To see which NixOS channel you’re
   subscribed to, run the following as root:
 <screen>
 <prompt># </prompt>nix-channel --list | grep nixos
@@ -75,13 +75,13 @@ nixos https://nixos.org/channels/nixos-unstable
 <prompt># </prompt>nix-channel --add https://nixos.org/channels/<replaceable>channel-name</replaceable> nixos
 </screen>
   (Be sure to include the <literal>nixos</literal> parameter at the end.) For
-  instance, to use the NixOS 20.03 stable channel:
+  instance, to use the NixOS 20.09 stable channel:
 <screen>
-<prompt># </prompt>nix-channel --add https://nixos.org/channels/nixos-20.03 nixos
+<prompt># </prompt>nix-channel --add https://nixos.org/channels/nixos-20.09 nixos
 </screen>
   If you have a server, you may want to use the “small” channel instead:
 <screen>
-<prompt># </prompt>nix-channel --add https://nixos.org/channels/nixos-20.03-small nixos
+<prompt># </prompt>nix-channel --add https://nixos.org/channels/nixos-20.09-small nixos
 </screen>
   And if you want to live on the bleeding edge:
 <screen>
@@ -132,7 +132,7 @@ nixos https://nixos.org/channels/nixos-unstable
    kernel, initrd or kernel modules.
    You can also specify a channel explicitly, e.g.
 <programlisting>
-<xref linkend="opt-system.autoUpgrade.channel"/> = https://nixos.org/channels/nixos-20.03;
+<xref linkend="opt-system.autoUpgrade.channel"/> = https://nixos.org/channels/nixos-20.09;
 </programlisting>
   </para>
  </section>
diff --git a/nixos/doc/manual/man-nixos-install.xml b/nixos/doc/manual/man-nixos-install.xml
index b205e23096875..91542d37cbd55 100644
--- a/nixos/doc/manual/man-nixos-install.xml
+++ b/nixos/doc/manual/man-nixos-install.xml
@@ -50,6 +50,12 @@
    </arg>
 
    <arg>
+    <group choice='req'>
+     <arg choice='plain'><option>--impure</option></arg>
+    </group>
+   </arg>
+
+   <arg>
      <arg choice='plain'>
        <option>--channel</option>
      </arg>
@@ -102,6 +108,12 @@
 
    <arg>
     <arg choice='plain'>
+     <option>--keep-going</option>
+    </arg>
+   </arg>
+
+   <arg>
+    <arg choice='plain'>
      <option>--help</option>
     </arg>
    </arg>
@@ -297,6 +309,17 @@
    </varlistentry>
    <varlistentry>
     <term>
+     <option>--keep-going</option>
+    </term>
+    <listitem>
+     <para>
+      Causes Nix to continue building derivations as far as possible
+      in the face of failed builds.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
      <option>--help</option>
     </term>
     <listitem>
diff --git a/nixos/doc/manual/manual.xml b/nixos/doc/manual/manual.xml
index 18a67a2dd9416..db9e7313831da 100644
--- a/nixos/doc/manual/manual.xml
+++ b/nixos/doc/manual/manual.xml
@@ -19,5 +19,6 @@
   <xi:include href="./generated/options-db.xml"
                 xpointer="configuration-variable-list" />
  </appendix>
+ <xi:include href="contributing-to-this-manual.xml" />
  <xi:include href="release-notes/release-notes.xml" />
 </book>
diff --git a/nixos/doc/manual/preface.xml b/nixos/doc/manual/preface.xml
index 6ac9ae7e7861d..0f7db6ef1a826 100644
--- a/nixos/doc/manual/preface.xml
+++ b/nixos/doc/manual/preface.xml
@@ -21,7 +21,11 @@
    xlink:href="https://discourse.nixos.org">Discourse</literal> or
   on the <link
    xlink:href="irc://irc.freenode.net/#nixos">
-  <literal>#nixos</literal> channel on Freenode</link>. Bugs should be
+  <literal>#nixos</literal> channel on Freenode</link>, or
+  consider
+  <link
+   xlink:href="#chap-contributing">
+   contributing to this manual</link>. Bugs should be
   reported in
   <link
    xlink:href="https://github.com/NixOS/nixpkgs/issues">NixOS’
diff --git a/nixos/doc/manual/release-notes/release-notes.xml b/nixos/doc/manual/release-notes/release-notes.xml
index bf18457c2b385..e083d51406c76 100644
--- a/nixos/doc/manual/release-notes/release-notes.xml
+++ b/nixos/doc/manual/release-notes/release-notes.xml
@@ -8,7 +8,7 @@
   This section lists the release notes for each stable version of NixOS and
   current unstable revision.
  </para>
- <xi:include href="rl-2103.xml" />
+ <xi:include href="rl-2105.xml" />
  <xi:include href="rl-2009.xml" />
  <xi:include href="rl-2003.xml" />
  <xi:include href="rl-1909.xml" />
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index 87f1228561949..4206f44f6c77a 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -45,6 +45,15 @@
    </listitem>
    <listitem>
     <para>
+     Grub is updated to 2.04, adding support for booting from F2FS filesystems and
+     Btrfs volumes using zstd compression. Note that some users have been unable
+to boot after upgrading to 2.04 - for more information, please see <link
+xlink:href="https://github.com/NixOS/nixpkgs/issues/61718#issuecomment-617618503">this
+     discussion</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      Postgresql for NixOS service now defaults to v11.
     </para>
    </listitem>
@@ -650,7 +659,7 @@ See https://github.com/NixOS/nixpkgs/pull/71684 for details.
       <listitem>
        <para>
          <literal>boot.extraTTYs</literal> renamed to
-         <link linkend="opt-console.extraTTYs">console.extraTTYs</link>
+         <literal>console.extraTTYs</literal>.
        </para>
       </listitem>
     </itemizedlist>
diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index 486e527776ef8..a6cff1a8faedd 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -3,8 +3,13 @@
          xmlns:xi="http://www.w3.org/2001/XInclude"
          version="5.0"
          xml:id="sec-release-20.09">
- <title>Release 20.09 (“Nightingale”, 2020.09/??)</title>
+ <title>Release 20.09 (“Nightingale”, 2020.10/27)</title>
 
+  <para>
+   Support is planned until the end of June 2021, handing over to 21.05.
+   (Plans <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md#core-changes">
+   have shifted</link> by two months since release of 20.09.)
+  </para>
  <section xmlns="http://docbook.org/ns/docbook"
          xmlns:xlink="http://www.w3.org/1999/xlink"
          xmlns:xi="http://www.w3.org/2001/XInclude"
@@ -13,88 +18,611 @@
   <title>Highlights</title>
 
   <para>
-   In addition to numerous new and upgraded packages, this release has the
+   In addition to 7349 new, 14442 updated, and 8181 removed packages, this release has the
    following highlights:
   </para>
 
   <itemizedlist>
    <listitem>
     <para>
-     Support is planned until the end of April 2021, handing over to 21.03.
+     Core version changes:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       gcc: 9.2.0 -> 9.3.0
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       glibc: 2.30 -> 2.31
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       linux: still defaults to 5.4.x, all supported kernels available
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       mesa: 19.3.5 -> 20.1.7
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
    <listitem>
-    <para>GNOME desktop environment was upgraded to 3.36, see its <link xlink:href="https://help.gnome.org/misc/release-notes/3.36/">release notes</link>.</para>
-   </listitem>
-   <listitem>
-     <para>
-     The Cinnamon desktop environment (v4.6) has been added. <varname>services.xserver.desktopManager.cinnamon.enable = true;</varname> to try it out!
-     Remember that, with any new feature it's possible you could run into issues, so please send all support requests to <link xlink:href="https://github.com/NixOS/nixpkgs/issues">github.com/NixOS/nixpkgs</link> to notify the maintainers.
-     </para>
+    <para>
+     Desktop Environments:
+    </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       plasma5: 5.17.5 -> 5.18.5
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       kdeApplications: 19.12.3 -> 20.08.1
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       gnome3: 3.34 -> 3.36, see its <link xlink:href="https://help.gnome.org/misc/release-notes/3.36/">release notes</link>
+      </para>
+     </listitem>
+      <listitem>
+       <para>
+        cinnamon: added at 4.6
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       NixOS now distributes an official <link xlink:href="https://nixos.org/download.html#nixos-iso">GNOME ISO</link>
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
+
    <listitem>
     <para>
-      Quickly configure a complete, private, self-hosted video
-      conferencing solution with the new Jitsi Meet module.
+     Programming Languages and Frameworks:
     </para>
+    <itemizedlist>
+
+     <listitem>
+      <para>
+       Agda ecosystem was heavily reworked (see more details below)
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       PHP now defaults to PHP 7.4, updated from 7.3
+      </para>
+     </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>
+       Python 3.5 reached its upstream EOL at the end of September 2020: it
+       has been removed from the list of available packages
+      </para>
+     </listitem>
+    </itemizedlist>
    </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.
+     Databases and Service Monitoring:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       MariaDB has been updated to 10.4, MariaDB Galera to 26.4. Please read the related upgrade instructions under <link linkend="sec-release-20.09-incompatibilities">backwards incompatibilities</link> before upgrading.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+        Zabbix now defaults to 5.0, updated from 4.4. Please read related sections under <link linkend="sec-release-20.09-incompatibilities">backwards compatibilities</link> before upgrading.
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
+
    <listitem>
     <para>
-     We now distribute a GNOME ISO.
+     Major module changes:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       Quickly configure a complete, private, self-hosted video
+       conferencing solution with the new Jitsi Meet module.
+      </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>
+       in your <link linkend="opt-services.openssh.extraConfig">services.openssh.extraConfig</link> you should
+       make use of these new options instead.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       There is a new module for Podman (<varname>virtualisation.podman</varname>), a drop-in replacement for the Docker command line.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       The new <varname>virtualisation.containers</varname> module manages configuration shared by the CRI-O and Podman modules.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+        Declarative Docker containers are renamed from <varname>docker-containers</varname> to <varname>virtualisation.oci-containers.containers</varname>.
+        This is to make it possible to use <literal>podman</literal> instead of <literal>docker</literal>.
+      </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>
+         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 <link xlink:href="https://sourceforge.net/projects/g15daemon/">g15daemon project</link>.
+       </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 available in GRUB version 2.
+       </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
+
    <listitem>
     <para>
-     PHP now defaults to PHP 7.4, updated from 7.3.
+     NixOS module changes:
     </para>
+    <itemizedlist>
+     <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>
+       Following its deprecation in 20.03, the Perl NixOS test driver has been removed.
+       All remaining tests have been ported to the Python test framework.
+       Code outside nixpkgs using <filename>make-test.nix</filename> or
+       <filename>testing.nix</filename> needs to be ported to
+       <filename>make-test-python.nix</filename> and
+       <filename>testing-python.nix</filename> respectively.
+      </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>
+    </itemizedlist>
    </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.
+     Starting with this release, the hydra-build-result
+     <literal>nixos-<replaceable>YY.MM</replaceable></literal>
+     branches no longer exist in the <link
+     xlink:href="https://github.com/nixos/nixpkgs-channels">deprecated
+     nixpkgs-channels repository</link>.  These branches are now in
+     <link xlink:href="https://github.com/nixos/nixpkgs">the main nixpkgs
+     repository</link>.
     </para>
    </listitem>
+  </itemizedlist>
+ </section>
+
+ <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-release-20.09-new-services">
+  <title>New Services</title>
+
+  <para>
+   In addition to 1119 new, 118 updated, and 476 removed options; 61 new modules were added since the last release:
+  </para>
+
+  <itemizedlist>
    <listitem>
     <para>
-     Python 3 now defaults to Python 3.8 instead of 3.7.
+       Hardware:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-hardware.system76.firmware-daemon.enable" /> adds easy support of system76 firmware
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-hardware.uinput.enable" /> loads uinput kernel module
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-hardware.video.hidpi.enable" /> enable good defaults for HiDPI displays
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-hardware.wooting.enable" /> support for Wooting keyboards
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-hardware.xpadneo.enable" /> xpadneo driver for Xbox One wireless controllers
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
    <listitem>
     <para>
-     Python 3.5 has reached its upstream EOL at the end of September 2020: it
-     has been removed from the list of available packages.
+       Programs:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-programs.hamster.enable" /> enable hamster time tracking
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-programs.steam.enable" /> adds easy enablement of steam and related system configuration
+      </para>
+     </listitem>
+    </itemizedlist>
    </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>
-     in your <link linkend="opt-services.openssh.extraConfig">services.openssh.extraConfig</link> you should
-     make use of these new options instead.
+       Security:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-security.doas.enable" /> alternative to sudo, allows non-root users to execute commands as root
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-security.tpm2.enable" /> add Trusted Platform Module 2 support
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
    <listitem>
     <para>
-     There is a new module for Podman(<varname>virtualisation.podman</varname>), a drop-in replacement for the Docker command line.
+       System:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-boot.initrd.network.openvpn.enable" /> start an OpenVPN client during initrd boot
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
    <listitem>
     <para>
-     The new <varname>virtualisation.containers</varname> module manages configuration shared by the CRI-O and Podman modules.
+       Virtualization:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-boot.enableContainers" /> use nixos-containers
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-virtualisation.oci-containers.containers" /> run OCI (Docker) containers
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-virtualisation.podman.enable" /> daemonless container engine
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
+
    <listitem>
     <para>
-      Declarative Docker containers are renamed from <varname>docker-containers</varname> to <varname>virtualisation.oci-containers.containers</varname>.
-      This is to make it possible to use <literal>podman</literal> instead of <literal>docker</literal>.
+       Services:
     </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.ankisyncd.enable" /> Anki sync server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.bazarr.enable" /> Subtitle manager for Sonarr and Radarr
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.biboumi.enable" /> Biboumi XMPP gateway to IRC
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.blockbook-frontend" /> Blockbook-frontend, a service for the Trezor wallet
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.cage.enable" /> Wayland cage service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.convos.enable" /> IRC daemon, which can be accessed throught the browser
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.engelsystem.enable" /> Tool for coordinating volunteers and shifts on large events
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.espanso.enable" /> text-expander written in rust
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.foldingathome.enable" /> Folding@home client
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.gerrit.enable" /> Web-based team code collaboration tool
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.go-neb.enable" /> Matrix bot
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.hardware.xow.enable" /> xow as a systemd service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.hercules-ci-agent.enable" /> Hercules CI build agent
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.jicofo.enable" /> Jitsi Conference Focus, component of Jitsi Meet
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.jirafeau.enable" /> A web file repository
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.jitsi-meet.enable" /> Secure, simple and scalable video conferences
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.jitsi-videobridge.enable" /> Jitsi Videobridge, a WebRTC compatible router
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.jupyterhub.enable" /> Jupyterhub development server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.k3s.enable" /> Lightweight Kubernetes distribution
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.magic-wormhole-mailbox-server.enable" /> Magic Wormhole Mailbox Server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.malcontent.enable" /> Parental Control support
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.matrix-appservice-discord.enable" /> Matrix and Discord bridge
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.mautrix-telegram.enable" /> Matrix-Telegram puppeting/relaybot bridge
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.mirakurun.enable" /> Japanese DTV Tuner Server Service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.molly-brown.enable" /> Molly-Brown Gemini server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.mullvad-vpn.enable" /> Mullvad VPN daemon
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.ncdns.enable" /> Namecoin to DNS bridge
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.nextdns.enable" /> NextDNS to DoH Proxy service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.nix-store-gcs-proxy" /> Google storage bucket to be used as a nix store
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.onedrive.enable" /> OneDrive sync service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.pinnwand.enable" /> Pastebin-like service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.pixiecore.enable" /> Manage network booting of machines
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.privacyidea.enable" /> Privacy authentication server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.quorum.enable" /> Quorum blockchain daemon
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.robustirc-bridge.enable" /> RobustIRC bridge
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.rss-bridge.enable" /> Generate RSS and Atom feeds
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.rtorrent.enable" /> rTorrent service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.smartdns.enable" /> SmartDNS DNS server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.sogo.enable" /> SOGo groupware
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.teeworlds.enable" /> Teeworlds game server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.torque.mom.enable" /> torque computing node
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.torque.server.enable" /> torque server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.tuptime.enable" /> A total uptime service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.urserver.enable" /> X11 remote server
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.wasabibackend.enable" /> Wasabi backend service
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.yubikey-agent.enable" /> Yubikey agent
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       <xref linkend="opt-services.zigbee2mqtt.enable" /> Zigbee to MQTT bridge
+      </para>
+     </listitem>
+    </itemizedlist>
    </listitem>
+
+  </itemizedlist>
+
+ </section>
+
+ <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-release-20.09-incompatibilities">
+  <title>Backward Incompatibilities</title>
+
+  <para>
+   When upgrading from a previous release, please be aware of the following
+   incompatible changes:
+  </para>
+
+  <itemizedlist>
    <listitem>
     <para>
       MariaDB has been updated to 10.4, MariaDB Galera to 26.4.
@@ -144,36 +672,7 @@ GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
       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
@@ -208,72 +707,13 @@ GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
 </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>
-   <listitem>
-     <para>
-       Following its deprecation in 20.03, the Perl NixOS test driver has been removed.
-       All remaining tests have been ported to the Python test framework.
-       Code outside nixpkgs using <filename>make-test.nix</filename> or
-       <filename>testing.nix</filename> needs to be ported to
-       <filename>make-test-python.nix</filename> and
-       <filename>testing-python.nix</filename> respectively.
-     </para>
-   </listitem>
-  </itemizedlist>
- </section>
 
- <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-release-20.09-new-services">
-  <title>New Services</title>
-
-  <para>
-   The following new services were added since the last release:
-  </para>
-
-  <itemizedlist>
    <listitem>
     <para>
-      There is a new <xref linkend="opt-security.doas.enable"/> module that provides <command>doas</command>, a lighter alternative to <command>sudo</command> with many of the same features.
-    </para>
-  </listitem>
-  <listitem>
-    <para>
-      <link xlink:href="https://hercules-ci.com">Hercules CI</link> Agent is a specialized build agent for projects built with Nix. See the <link xlink:href="https://nixos.org/nixos/options.html#services.hercules-ci-agent">options</link> and <link xlink:href="https://docs.hercules-ci.com/hercules-ci/getting-started/#deploy-agent">setup</link>.
+    <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>
-  </itemizedlist>
-
- </section>
-
- <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-release-20.09-incompatibilities">
-  <title>Backward Incompatibilities</title>
-
-  <para>
-   When upgrading from a previous release, please be aware of the following
-   incompatible changes:
-  </para>
-
-  <itemizedlist>
    <listitem>
     <para>
      The <link linkend="opt-services.matrix-synapse.enable">matrix-synapse</link> module no longer includes optional dependencies by default, they have to be added through the <link linkend="opt-services.matrix-synapse.plugins">plugins</link> option.
@@ -300,7 +740,7 @@ GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
      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;
+    phantomJsSupport = true;
   });
 }</programlisting>
     </para>
@@ -451,12 +891,23 @@ php.override {
    <listitem>
      <para>
        Nginx web server now starting with additional sandbox/hardening options. By default, write access
-       to <literal>services.nginx.stateDir</literal> is allowed. To allow writing to other folders,
+       to <literal>/var/log/nginx</literal> and <literal>/var/cache/nginx</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>
+     <para>
+       Nginx is also started with the systemd option <literal>ProtectHome = mkDefault true;</literal>
+       which forbids it to read anything from <literal>/home</literal>, <literal>/root</literal>
+       and <literal>/run/user</literal> (see
+       <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=">ProtectHome docs</link>
+       for details).
+       If you require serving files from home directories, you may choose to set e.g.
+<programlisting>
+systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
+</programlisting>
+     </para>
    </listitem>
    <listitem>
     <para>
@@ -643,6 +1094,13 @@ systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ];
      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>
+         <literal>sslh</literal> has been updated to version
+           <literal>1.21</literal>. The <literal>ssl</literal> probe must be
+             renamed to <literal>tls</literal> in <xref linkend="opt-services.sslh.appendConfig"/>.
+    </para>
+  </listitem>
    <listitem>
     <para>
      Users of <link xlink:href="http://openafs.org">OpenAFS 1.6</link> must
@@ -851,6 +1309,13 @@ CREATE ROLE postgres LOGIN SUPERUSER;
    </listitem>
    <listitem>
     <para>
+     The <literal>hardware.nvidia.optimus_prime.enable</literal> service has been renamed to
+     <literal>hardware.nvidia.prime.sync.enable</literal> and has many new enhancements.
+     Related nvidia prime settings may have also changed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      The package <package>nextcloud17</package> has been removed and <package>nextcloud18</package> was marked as insecure
      since both of them will <link xlink:href="https://docs.nextcloud.com/server/19/admin_manual/release_schedule.html">
      will be EOL (end of life) within the lifetime of 20.09</link>.
@@ -880,6 +1345,20 @@ CREATE ROLE postgres LOGIN SUPERUSER;
       It was chosen to do this as it has a usability breaking issue (see issue <link xlink:href="https://github.com/NixOS/nixpkgs/issues/98819">#98819</link>)
       that makes it unsuitable to be a default app.
     </para>
+    <note>
+      <para>
+        Issue <link
+        xlink:href="https://github.com/NixOS/nixpkgs/issues/98819">#98819</link>
+        is now fixed and <package>gnome3.epiphany</package> is once
+        again installed by default.
+      </para>
+    </note>
+   </listitem>
+   <listitem>
+     <para>
+       If you want to manage the configuration of <package>wpa_supplicant</package> outside of NixOS you must ensure that none of <xref linkend="opt-networking.wireless.networks" />, <xref linkend="opt-networking.wireless.extraConfig" /> or <xref linkend="opt-networking.wireless.userControlled.enable" /> is being used or <literal>true</literal>.
+       Using any of those options will cause <package>wpa_supplicant</package> to be started with a NixOS generated configuration file instead of your own.
+    </para>
    </listitem>
   </itemizedlist>
  </section>
@@ -1061,7 +1540,7 @@ services.transmission.settings.rpc-bind-address = "0.0.0.0";
    <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>
+     as a list is deprecated and will break eval in 21.05. Instead, an attribute-set should be specified where the <literal>name</literal>
      should be the key of the attribute.
     </para>
     <para>
@@ -1095,6 +1574,8 @@ services.transmission.settings.rpc-bind-address = "0.0.0.0";
      <package>nextcloud18</package> before upgrading to <package>nextcloud19</package>
      since Nextcloud doesn't support upgrades across multiple major versions.
     </para>
+   </listitem>
+   <listitem>
      <para>
        The <literal>nixos-run-vms</literal> script now deletes the
        previous run machines states on test startup. You can use the
@@ -1138,30 +1619,30 @@ services.transmission.settings.rpc-bind-address = "0.0.0.0";
      <para>
       Agda has been heavily reworked.
       <itemizedlist>
-	<listitem>
-	  <para>
-	    <literal>agda.mkDerivation</literal> has been heavily changed and
-	    is now located at <package>agdaPackages.mkDerivation</package>.
-	  </para>
-	</listitem>
-	<listitem>
-	  <para>
-	    New top-level packages <package>agda</package> and
-	    <literal>agda.withPackages</literal> have been added, the second
-	    of which sets up agda with access to chosen libraries.
-	  </para>
-	</listitem>
-	<listitem>
-	  <para>
-	    All agda libraries now live under
-	    <literal>agdaPackages</literal>.
-	  </para>
-	</listitem>
-	<listitem>
-	  <para>
-	    Many broken libraries have been removed.
-	  </para>
-	</listitem>
+       <listitem>
+         <para>
+           <literal>agda.mkDerivation</literal> has been heavily changed and
+           is now located at <package>agdaPackages.mkDerivation</package>.
+         </para>
+       </listitem>
+       <listitem>
+         <para>
+           New top-level packages <package>agda</package> and
+           <literal>agda.withPackages</literal> have been added, the second
+           of which sets up agda with access to chosen libraries.
+         </para>
+       </listitem>
+       <listitem>
+         <para>
+           All agda libraries now live under
+           <literal>agdaPackages</literal>.
+         </para>
+       </listitem>
+       <listitem>
+         <para>
+           Many broken libraries have been removed.
+         </para>
+       </listitem>
       </itemizedlist>
       See the <link
       xlink:href="https://nixos.org/nixpkgs/manual/#agda">new
@@ -1174,7 +1655,7 @@ services.transmission.settings.rpc-bind-address = "0.0.0.0";
       nixpkgs. It was a work in progress to package the
       <link xlink:href="https://www.deepin.org/en/dde/">Deepin Desktop Environment (DDE)</link>,
       including libraries, tools and applications, and it was still
-      missing a service to lauch the desktop environment. It has shown
+      missing a service to launch the desktop environment. It has shown
       to no longer be a feasible goal due to reasons discussed in
       <link xlink:href="https://github.com/NixOS/nixpkgs/issues/94870">issue #94870</link>.
       The package <literal>netease-cloud-music</literal> has also been
@@ -1219,4 +1700,131 @@ services.transmission.settings.rpc-bind-address = "0.0.0.0";
     </listitem>
   </itemizedlist>
  </section>
+
+ <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-release-20.09-contributions">
+  <title>Contributions</title>
+  <para>
+        I, Jonathan Ringer, would like to thank the following individuals for their work on nixpkgs. This release could not be done without the hard work of the NixOS community. There were 31282 contributions across 1313 contributors.
+  </para>
+  <orderedlist>
+   <para>
+    Top contributors to NixOS/Nixpkgs from the 20.03 release to the 20.09 release:
+   </para>
+   <listitem>
+    <para>
+  2288  Mario Rodas
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+  1837  Frederik Rietdijk
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   946  Jörg Thalheim
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   925  Maximilian Bosch
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   687  Jonathan Ringer
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   651  Jan Tojnar
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   622  Daniël de Kok
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   605  WORLDofPEACE
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   597  Florian Klink
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   528  José Romildo Malaquias
+    </para>
+   </listitem>
+  </orderedlist>
+
+  <orderedlist>
+   <para>
+    Top contributors to stabilizing this release (Zero Hydra Failures period):
+   </para>
+   <listitem>
+    <para>
+      281  volth
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+   101  Robert Scott
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    86  Tim Steinbach
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    76  WORLDofPEACE
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    49  Maximilian Bosch
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    42  Thomas Tuegel
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    37  Doron Behar
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    36  Vladimír Čunát
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    27  Jonathan Ringer
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    27  Maciej Krüger
+    </para>
+   </listitem>
+  </orderedlist>
+
+  <para>
+   I, Jonathan Ringer, would also like to personally thank @WORLDofPEACE for their help in mentoring me on the release process. Special thanks also goes to Thomas Tuegel for helping immensely with stabilizing Qt, KDE, and Plasma5; I would also like to thank Robert Scott for his numerous fixes and pull request reviews.
+   </para>
+
+ </section>
 </section>
diff --git a/nixos/doc/manual/release-notes/rl-2103.xml b/nixos/doc/manual/release-notes/rl-2103.xml
deleted file mode 100644
index 26da246076ad2..0000000000000
--- a/nixos/doc/manual/release-notes/rl-2103.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-<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-release-21.03">
- <title>Release 21.03 (“Okapi”, 2021.03/??)</title>
-
- <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-release-21.03-highlights">
-  <title>Highlights</title>
-
-  <para>
-   In addition to numerous new and upgraded packages, this release has the
-   following highlights:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Support is planned until the end of October 2021, handing over to 21.09.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
-
- <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-release-21.03-new-services">
-  <title>New Services</title>
-
-  <para>
-   The following new services were added since the last release:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para />
-   </listitem>
-  </itemizedlist>
-
- </section>
-
- <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-release-21.03-incompatibilities">
-  <title>Backward Incompatibilities</title>
-
-  <para>
-   When upgrading from a previous release, please be aware of the following
-   incompatible changes:
-  </para>
-
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>systemd-journal2gelf</literal> no longer parses json and expects the receiving system to handle it. How to achieve this with Graylog is described in this <link xlink:href="https://github.com/parse-nl/SystemdJournal2Gelf/issues/10">GitHub issue</link>.
-      </para>
-   </listitem>
-   <listitem>
-    <para>
-      The option <option>fonts.enableFontDir</option> has been renamed to
-      <xref linkend="opt-fonts.fontDir.enable"/>. The path of font directory
-      has also been changed to <literal>/run/current-system/sw/share/X11/fonts</literal>,
-      for consistency with other X11 resources.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
-
- <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-release-21.03-notable-changes">
-  <title>Other Notable Changes</title>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     The default-version of <literal>nextcloud</literal> is <package>nextcloud20</package>.
-     Please note that it's <emphasis>not</emphasis> possible to upgrade <literal>nextcloud</literal>
-     across multiple major versions! This means that it's e.g. not possible to upgrade
-     from <package>nextcloud18</package> to <package>nextcloud20</package> in a single deploy.
-    </para>
-    <para>
-     The package can be manually upgraded by setting <xref linkend="opt-services.nextcloud.package" />
-     to <package>nextcloud20</package>.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
-</section>
diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml
new file mode 100644
index 0000000000000..ca4b468e355fc
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2105.xml
@@ -0,0 +1,729 @@
+<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-release-21.05">
+ <title>Release 21.05 (“Okapi”, 2021.05/??)</title>
+
+ <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-release-21.05-highlights">
+  <title>Highlights</title>
+
+  <para>
+   In addition to numerous new and upgraded packages, this release has the
+   following highlights:
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     Support is planned until the end of December 2021, handing over to 21.11.
+    </para>
+   </listitem>
+   <listitem>
+    <para>GNOME desktop environment was upgraded to 3.38, see its <link xlink:href="https://help.gnome.org/misc/release-notes/3.38/">release notes</link>.</para>
+   </listitem>
+   <listitem>
+    <para>
+     <link xlink:href="https://www.gnuradio.org/">GNURadio</link> 3.8 was
+     <link xlink:href="https://github.com/NixOS/nixpkgs/issues/82263">finnally</link>
+     packaged, along with a rewrite to the Nix expressions, allowing users to
+     override the features upstream supports selecting to compile or not to.
+     Additionally, the attribute <code>gnuradio</code> and <code>gnuradio3_7</code>
+     now point to an externally wrapped by default derivations, that allow you to
+     also add `extraPythonPackages` to the Python interpreter used by GNURadio.
+     Missing environmental variables needed for operational GUI were also added
+     (<link xlink:href="https://github.com/NixOS/nixpkgs/issues/75478">#7547</link>).
+    </para>
+   </listitem>
+  </itemizedlist>
+ </section>
+
+ <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-release-21.05-new-services">
+  <title>New Services</title>
+
+  <para>
+   The following new services were added since the last release:
+  </para>
+
+  <itemizedlist>
+   <listitem>
+     <para>
+       <link xlink:href="https://www.keycloak.org/">Keycloak</link>,
+       an open source identity and access management server with
+       support for <link
+       xlink:href="https://openid.net/connect/">OpenID Connect</link>,
+       <link xlink:href="https://oauth.net/2/">OAUTH 2.0</link> and
+       <link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
+       2.0</link>.
+     </para>
+     <para>
+       See the <link linkend="module-services-keycloak">Keycloak
+       section of the NixOS manual</link> for more information.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       <xref linkend="opt-services.samba-wsdd.enable" /> Web Services Dynamic Discovery host daemon
+     </para>
+   </listitem>
+  </itemizedlist>
+
+ </section>
+
+ <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-release-21.05-incompatibilities">
+  <title>Backward Incompatibilities</title>
+
+  <para>
+   When upgrading from a previous release, please be aware of the following
+   incompatible changes:
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     If you are using <option>services.udev.extraRules</option> to assign
+     custom names to network interfaces, this may stop working due to a change
+     in the initialisation of dhcpcd and systemd networkd. To avoid this, either
+     move them to <option>services.udev.initrdRules</option> or see the new
+     <link linkend="sec-custom-ifnames">Assigning custom names</link> section
+     of the NixOS manual for an example using networkd links.
+    </para>
+   </listitem>
+    <listitem>
+      <para>
+        The <literal>systemConfig</literal> kernel parameter is no longer added to boot loader entries. It has been unused since September 2010, but if do have a system generation from that era, you will now be unable to boot into them.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        <literal>systemd-journal2gelf</literal> no longer parses json and expects the receiving system to handle it. How to achieve this with Graylog is described in this <link xlink:href="https://github.com/parse-nl/SystemdJournal2Gelf/issues/10">GitHub issue</link>.
+      </para>
+   </listitem>
+   <listitem>
+    <para>
+     If the <varname>services.dbus</varname> module is enabled, then
+     the user D-Bus session is now always socket activated. The
+     associated options <varname>services.dbus.socketActivated</varname>
+     and <varname>services.xserver.startDbusSession</varname> have
+     therefore been removed and you will receive a warning if
+     they are present in your configuration. This change makes the
+     user D-Bus session available also for non-graphical logins.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <varname>networking.wireless.iwd</varname> module now installs
+     the upstream-provided 80-iwd.link file, which sets the NamePolicy=
+     for all wlan devices to "keep kernel", to avoid race conditions
+     between iwd and networkd. If you don't want this, you can set
+     <literal>systemd.network.links."80-iwd" = lib.mkForce {}</literal>.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       <literal>rubyMinimal</literal> was removed due to being unused and
+       unusable. The default ruby interpreter includes JIT support, which makes
+       it reference it's compiler. Since JIT support is probably needed by some
+       Gems, it was decided to enable this feature with all cc references by
+       default, and allow to build a Ruby derivation without references to cc,
+       by setting <literal>jitSupport = false;</literal> in an overlay. See
+       <link xlink:href="https://github.com/NixOS/nixpkgs/pull/90151">#90151</link>
+       for more info.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       Setting <option>services.openssh.authorizedKeysFiles</option> now also affects which keys <option>security.pam.enableSSHAgentAuth</option> will use.
+
+       WARNING: If you are using these options in combination do make sure that any key paths you use are present in <option>services.openssh.authorizedKeysFiles</option>!
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The option <option>fonts.enableFontDir</option> has been renamed to
+     <xref linkend="opt-fonts.fontDir.enable"/>. The path of font directory
+     has also been changed to <literal>/run/current-system/sw/share/X11/fonts</literal>,
+     for consistency with other X11 resources.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      A number of options have been renamed in the kicad interface. <literal>oceSupport</literal>
+      has been renamed to <literal>withOCE</literal>, <literal>withOCCT</literal> has been renamed
+      to <literal>withOCC</literal>, <literal>ngspiceSupport</literal> has been renamed to
+      <literal>withNgspice</literal>, and <literal>scriptingSupport</literal> has been renamed to
+      <literal>withScripting</literal>. Additionally, <literal>kicad/base.nix</literal> no longer
+      provides default argument values since these are provided by
+      <literal>kicad/default.nix</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The socket for the <literal>pdns-recursor</literal> module was moved from <literal>/var/lib/pdns-recursor</literal>
+      to <literal>/run/pdns-recursor</literal> to match upstream.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      Paperwork was updated to version 2. The on-disk format slightly changed,
+      and it is not possible to downgrade from Paperwork 2 back to Paperwork
+      1.3. Back your documents up before upgrading. See <link xlink:href="https://forum.openpaper.work/t/paperwork-2-0/112/5">this thread</link> for more details.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      PowerDNS has been updated from <literal>4.2.x</literal> to <literal>4.3.x</literal>. Please
+      be sure to review the <link xlink:href="https://doc.powerdns.com/authoritative/upgrading.html#x-to-4-3-0">Upgrade Notes</link>
+      provided by upstream before upgrading. Worth specifically noting is that the service now runs
+      entirely as a dedicated <literal>pdns</literal> user, instead of starting as <literal>root</literal>
+      and dropping privileges, as well as the default <literal>socket-dir</literal> location changing from
+      <literal>/var/lib/powerdns</literal> to <literal>/run/pdns</literal>.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       xfsprogs was update from 4.19 to 5.10. It now enables reflink support by default on filesystem creation.
+       Support for reflinks was added with an experimental status to kernel 4.9 and deemed stable in kernel 4.16.
+       If you want to be able to mount XFS filesystems created with this release of xfsprogs on kernel releases older than those, you need to format them
+       with <literal>mkfs.xfs -m reflink=0</literal>.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+      The uWSGI server is now built with POSIX capabilities. As a consequence,
+      root is no longer required in emperor mode and the service defaults to
+      running as the unprivileged <literal>uwsgi</literal> user. Any additional
+      capability can be added via the new option
+      <xref linkend="opt-services.uwsgi.capabilities"/>.
+      The previous behaviour can be restored by setting:
+<programlisting>
+  <xref linkend="opt-services.uwsgi.user"/> = "root";
+  <xref linkend="opt-services.uwsgi.group"/> = "root";
+  <xref linkend="opt-services.uwsgi.instance"/> =
+    {
+      uid = "uwsgi";
+      gid = "uwsgi";
+    };
+</programlisting>
+    </para>
+    <para>
+      Another incompatibility from the previous release is that vassals running under a
+      different user or group need to use <literal>immediate-{uid,gid}</literal>
+      instead of the usual <literal>uid,gid</literal> options.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    <package>btc1</package> has been abandoned upstream, and removed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    <package>cpp_ethereum</package> (aleth) has been abandoned upstream, and removed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    <package>riak-cs</package> package removed along with <varname>services.riak-cs</varname> module.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+    <package>stanchion</package> package removed along with <varname>services.stanchion</varname> module.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <package>mutt</package> has been updated to a new major version (2.x), which comes with
+     some backward incompatible changes that are described in the
+     <link xlink:href="http://www.mutt.org/relnotes/2.0/">release notes for Mutt 2.0</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      <literal>vim</literal> switched to Python 3, dropping all Python 2 support.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+      <link linkend="opt-boot.zfs.forceImportAll">boot.zfs.forceImportAll</link>
+      previously did nothing, but has been fixed. However its default has been
+      changed to <literal>false</literal> to preserve the existing default
+      behaviour. If you have this explicitly set to <literal>true</literal>,
+      please note that your non-root pools will now be forcibly imported.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <package>openafs</package> now points to <package>openafs_1_8</package>,
+     which is the new stable release.  OpenAFS 1.6 was removed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     MariaDB has been updated to 10.5.
+     Before you upgrade, it would be best to take a backup of your database and read
+     <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-104-to-mariadb-105/#incompatible-changes-between-104-and-105">
+     Incompatible Changes Between 10.4 and 10.5</link>.
+     After the upgrade you will need to run <literal>mysql_upgrade</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The TokuDB storage engine dropped in <package>mariadb</package> 10.5 and removed in <package>mariadb</package> 10.6.
+     It is recommended to switch to RocksDB. See also <link xlink:href="https://mariadb.com/kb/en/tokudb/">TokuDB</link> and
+     <link xlink:href="https://jira.mariadb.org/browse/MDEV-19780">MDEV-19780: Remove the TokuDB storage engine</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <literal>openldap</literal> module now has support for OLC-style
+      configuration, users of the <literal>configDir</literal> option may wish
+      to migrate. If you continue to use <literal>configDir</literal>, ensure that
+      <literal>olcPidFile</literal> is set to <literal>/run/slapd/slapd.pid</literal>.
+    </para>
+    <para>
+      As a result, <literal>extraConfig</literal> and <literal>extraDatabaseConfig</literal>
+      are removed. To help with migration, you can convert your <literal>slapd.conf</literal>
+      file to OLC configuration with the following script (find the location of this
+      configuration file by running <literal>systemctl status openldap</literal>, it is the
+      <literal>-f</literal> option.
+    </para>
+    <programlisting>
+      TMPDIR=$(mktemp -d)
+      slaptest -f /path/to/slapd.conf $TMPDIR
+      slapcat -F $TMPDIR -n0 -H 'ldap:///???(!(objectClass=olcSchemaConfig))'
+    </programlisting>
+    <para>
+      This will dump your current configuration in LDIF format, which should be
+      straightforward to convert into Nix settings. This does not show your schema
+      configuration, as this is unnecessarily verbose for users of the default schemas
+      and <literal>slaptest</literal> is buggy with schemas directly in the config file.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       Amazon EC2 and OpenStack Compute (nova) images now re-fetch instance meta data and user data from the instance
+       metadata service (IMDS) on each boot. For example: stopping an EC2 instance, changing its user data, and
+       restarting the instance will now cause it to fetch and apply the new user data.
+     </para>
+     <warning>
+       <para>
+         Specifically, <literal>/etc/ec2-metadata</literal> is re-populated on each boot. Some NixOS scripts that read
+         from this directory are guarded to only run if the files they want to manipulate do not already exist, and so
+         will not re-apply their changes if the IMDS response changes. Examples: <literal>root</literal>'s SSH key is
+         only added if <literal>/root/.ssh/authorized_keys</literal> does not exist, and SSH host keys are only set from
+         user data if they do not exist in <literal>/etc/ssh</literal>.
+       </para>
+     </warning>
+   </listitem>
+   <listitem>
+    <para>
+      The <literal>rspamd</literal> services is now sandboxed. It is run as
+      a dynamic user instead of root, so secrets and other files may have to
+      be moved or their permissions may have to be fixed. The sockets are now
+      located in <literal>/run/rspamd</literal> instead of <literal>/run</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      Enabling the Tor client no longer silently also enables and
+      configures Privoxy, and the
+      <varname>services.tor.client.privoxy.enable</varname> option has
+      been removed.  To enable Privoxy, and to configure it to use
+      Tor's faster port, use the following configuration:
+    </para>
+    <programlisting>
+      <xref linkend="opt-services.privoxy.enable" /> = true;
+      <xref linkend="opt-services.privoxy.enableTor" /> = true;
+    </programlisting>
+   </listitem>
+   <listitem>
+    <para>
+      The <literal>services.tor</literal> module has a new exhaustively typed <xref linkend="opt-services.tor.settings" /> option following RFC 0042; backward compatibility with old options has been preserved when aliasing was possible.
+      The corresponding systemd service has been hardened,
+      but there is a chance that the service still requires more permissions,
+      so please report any related trouble on the bugtracker.
+      Onion services v3 are now supported in <xref linkend="opt-services.tor.relay.onionServices" />.
+      A new <xref linkend="opt-services.tor.openFirewall" /> option as been introduced for allowing connections on all the TCP ports configured.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The options <literal>services.slurm.dbdserver.storagePass</literal>
+       and <literal>services.slurm.dbdserver.configFile</literal> have been removed.
+       Use <literal>services.slurm.dbdserver.storagePassFile</literal> instead to provide the database password.
+       Extra config options can be given via the option <literal>services.slurm.dbdserver.extraConfig</literal>. The actual configuration file is created on the fly on startup of the service.
+       This avoids that the password gets exposed in the nix store.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The <literal>wafHook</literal> hook does not wrap Python anymore.
+       Packages depending on <literal>wafHook</literal> need to include any Python into their <literal>nativeBuildInputs</literal>.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       Starting with version 1.7.0, the project formerly named <literal>CodiMD</literal>
+       is now named <literal>HedgeDoc</literal>.
+       New installations will no longer use the old name for users, state directories and such, this needs to be considered when moving state to a more recent NixOS installation.
+       Based on <xref linkend="opt-system.stateVersion" />, existing installations will continue to work.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <package>fish-foreign-env</package> package has been replaced with
+     <package>fishPlugins.foreign-env</package>, in which the fish
+     functions have been relocated to the
+     <literal>vendor_functions.d</literal> directory to be loaded automatically.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The prometheus json exporter is now managed by the prometheus community. Together with additional features
+       some backwards incompatibilities were introduced.
+       Most importantly the exporter no longer accepts a fixed command-line parameter to specify the URL of the
+       endpoint serving JSON. It now expects this URL to be passed as an URL parameter, when scraping the exporter's
+       <literal>/probe</literal> endpoint.
+       In the prometheus scrape configuration the scrape target might look like this:
+       <programlisting>
+http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
+       </programlisting>
+     </para>
+     <para>
+       Existing configuration for the exporter needs to be updated, but can partially be re-used.
+       Documentation is available in the upstream repository and a small example for NixOS is available
+       in the corresponding NixOS test.
+     </para>
+     <para>
+       These changes also affect <xref linkend="opt-services.prometheus.exporters.rspamd.enable" />, which is
+       just a preconfigured instance of the json exporter.
+     </para>
+     <para>
+       For more information, take a look at the <link xlink:href="https://github.com/prometheus-community/json_exporter">
+       official documentation</link> of the json_exporter.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       Androidenv was updated, removing the <literal>includeDocs</literal> and <literal>lldbVersions</literal>
+       arguments. Docs only covered a single version of the Android SDK, LLDB is now bundled with the NDK,
+       and both are no longer available to download from the Android package repositories. Additionally, since
+       the package lists have been updated, some older versions of Android packages may not be bundled. If you
+       depend on older versions of Android packages, we recommend overriding the repo.
+     </para>
+     <para>
+       Android packages are now loaded from a repo.json file created by parsing Android repo XML files. The arguments
+       <literal>repoJson</literal> and <literal>repoXmls</literal> have been added to allow overriding the built-in
+       androidenv repo.json with your own. Additionally, license files are now written to allow compatibility
+       with Gradle-based tools, and the <literal>extraLicenses</literal> argument has been added to accept more
+       SDK licenses if your project requires it. See the androidenv documentation for more details.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The attribute <varname>mpi</varname> is now consistently used to
+       provide a default, system-wide MPI implementation.
+       The default implementation is openmpi, which has been used before by
+       all derivations affects by this change.
+       Note that all packages that have used <varname>mpi ? null</varname> in the input
+       for optional MPI builds, have been changed to the boolean input paramater
+       <varname>useMpi</varname> to enable building with MPI.
+
+       Building all packages with <varname>mpich</varname> instead
+       of the default <varname>openmpi</varname> can now be achived like this:
+       <programlisting>
+self: super:
+{
+ mpi = super.mpich;
+}
+       </programlisting>
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+      The Searx module has been updated with the ability to configure the
+      service declaratively and uWSGI integration.
+      The option <literal>services.searx.configFile</literal> has been renamed
+      to <xref linkend="opt-services.searx.settingsFile"/> for consistency with
+      the new <xref linkend="opt-services.searx.settings"/>. In addition, the
+      <literal>searx</literal> uid and gid reservations have been removed
+      since they were not necessary: the service is now running with a
+      dynamically allocated uid.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+      The libinput module has been updated with the ability to configure mouse and touchpad settings separately.
+      The options in <literal>services.xserver.libinput</literal> have been renamed to <literal>services.xserver.libinput.touchpad</literal>,
+      while there is a new <literal>services.xserver.libinput.mouse</literal> for mouse related configuration.
+     </para>
+     <para>
+      Since touchpad options no longer apply to all devices, you may want to replicate your touchpad configuration in
+      mouse section.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     ALSA OSS emulation (<varname>sound.enableOSSEmulation</varname>) is now disabled by default.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      Thinkfan as been updated to <literal>1.2.x</literal>, which comes with a
+      new YAML based configuration format. For this reason, several NixOS options
+      of the thinkfan module have been changed to non-backward compatible types.
+      In addition, a new <xref linkend="opt-services.thinkfan.settings"/> option has
+      been added.
+    </para>
+    <para>
+      Please read the <link xlink:href="https://github.com/vmatare/thinkfan#readme">
+      thinkfan documentation</link> before updating.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Adobe Flash Player support has been dropped from the tree. In particular,
+     the following packages no longer support it:
+     <itemizedlist>
+      <listitem><simpara><package>chromium</package></simpara></listitem>
+      <listitem><simpara><package>firefox</package></simpara></listitem>
+      <listitem><simpara><package>qt48</package></simpara></listitem>
+      <listitem><simpara><package>qt5.qtwebkit</package></simpara></listitem>
+     </itemizedlist>
+     Additionally, packages <package>flashplayer</package> and
+     <package>hal-flash</package> were removed along with the
+     <varname>services.flashpolicyd</varname> module.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <literal>security.rngd</literal> module has been removed.
+      It was disabled by default in 20.09 as it was functionally redundant
+      with krngd in the linux kernel. It is not necessary for any device that the kernel recognises
+      as an hardware RNG, as it will automatically run the krngd task to periodically collect random
+      data from the device and mix it into the kernel's RNG.
+    </para>
+   </listitem>
+  </itemizedlist>
+ </section>
+
+ <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-release-21.05-notable-changes">
+  <title>Other Notable Changes</title>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     <literal>stdenv.lib</literal> has been deprecated and will break
+     eval in 21.11.  Please use <literal>pkgs.lib</literal> instead.
+     See <link xlink:href="https://github.com/NixOS/nixpkgs/issues/108938">#108938</link>
+     for details.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The Mailman NixOS module (<literal>services.mailman</literal>) has a new
+     option <xref linkend="opt-services.mailman.enablePostfix" />, defaulting
+     to true, that controls integration with Postfix.
+    </para>
+    <para>
+     If this option is disabled, default MTA config becomes not set and you
+     should set the options in <literal>services.mailman.settings.mta</literal>
+     according to the desired configuration as described in
+     <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The default-version of <literal>nextcloud</literal> is <package>nextcloud20</package>.
+     Please note that it's <emphasis>not</emphasis> possible to upgrade <literal>nextcloud</literal>
+     across multiple major versions! This means that it's e.g. not possible to upgrade
+     from <package>nextcloud18</package> to <package>nextcloud20</package> in a single deploy.
+    </para>
+    <para>
+     The package can be manually upgraded by setting <xref linkend="opt-services.nextcloud.package" />
+     to <package>nextcloud20</package>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The setting <xref linkend="opt-services.redis.bind" /> defaults to <literal>127.0.0.1</literal> now, making Redis listen on the loopback interface only, and not all public network interfaces.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     NixOS now emits a deprecation warning if systemd's <literal>StartLimitInterval</literal> setting is used in a <literal>serviceConfig</literal> section instead of in a <literal>unitConfig</literal>; that setting is deprecated and now undocumented for the service section by systemd upstream, but still effective and somewhat buggy there, which can be confusing. See <link xlink:href="https://github.com/NixOS/nixpkgs/issues/45785">#45785</link> for details.
+    </para>
+    <para>
+     All services should use <xref linkend="opt-systemd.services._name_.startLimitIntervalSec" /> or <literal>StartLimitIntervalSec</literal> in <xref linkend="opt-systemd.services._name_.unitConfig" /> instead.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The Unbound DNS resolver service (<literal>services.unbound</literal>) has been refactored to allow reloading, control sockets and to fix startup ordering issues.
+    </para>
+
+    <para>
+     It is now possible to enable a local UNIX control socket for unbound by setting the <xref linkend="opt-services.unbound.localControlSocketPath" />
+     option.
+    </para>
+
+    <para>
+     Previously we just applied a very minimal set of restrictions and
+     trusted unbound to properly drop root privs and capabilities.
+    </para>
+
+    <para>
+     As of this we are (for the most part) just using the upstream
+     example unit file for unbound. The main difference is that we start
+     unbound as <literal>unbound</literal> user with the required capabilities instead of
+     letting unbound do the chroot &amp; uid/gid changes.
+    </para>
+
+    <para>
+     The upstream unit configuration this is based on is a lot stricter with
+     all kinds of permissions then our previous variant. It also came with
+     the default of having the <literal>Type</literal> set to <literal>notify</literal>, therefore we are now also
+     using the <literal>unbound-with-systemd</literal> package here. Unbound will start up,
+     read the configuration files and start listening on the configured ports
+     before systemd will declare the unit <literal>active (running)</literal>.
+     This will likely help with startup order and the occasional race condition during system
+     activation where the DNS service is started but not yet ready to answer
+     queries. Services depending on <literal>nss-lookup.target</literal> or <literal>unbound.service</literal>
+     are now be able to use unbound when those targets have been reached.
+    </para>
+
+    <para>
+     Aditionally to the much stricter runtime environmet the
+     <literal>/dev/urandom</literal> mount lines we previously had in the code (that would
+     randomly failed during the stop-phase) have been removed as systemd will take care of those for us.
+    </para>
+
+    <para>
+     The <literal>preStart</literal> script is now only required if we enabled the trust
+      anchor updates (which are still enabled by default).
+    </para>
+
+    <para>
+     Another benefit of the refactoring is that we can now issue reloads via
+     either <literal>pkill -HUP unbound</literal> and <literal>systemctl reload unbound</literal> to reload the
+     running configuration without taking the daemon offline. A prerequisite
+     of this was that unbound configuration is available on a well known path
+     on the file system. We are using the path <literal>/etc/unbound/unbound.conf</literal> as that is the
+     default in the CLI tooling which in turn enables us to use
+     <literal>unbound-control</literal> without passing a custom configuration location.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <literal>services.dnscrypt-proxy2</literal> module now takes the upstream's example configuration and updates it with the user's settings.
+
+     An option has been added to restore the old behaviour if you prefer to declare the configuration from scratch.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     NixOS now defaults to the unified cgroup hierarchy (cgroupsv2).
+     See the <link xlink:href="https://www.redhat.com/sysadmin/fedora-31-control-group-v2">Fedora Article for 31</link>
+     for details on why this is desirable, and how it impacts containers.
+    </para>
+    <para>
+     If you want to run containers with a runtime that does not yet support cgroupsv2,
+     you can switch back to the old behaviour by setting
+     <xref linkend="opt-systemd.enableUnifiedCgroupHierarchy"/> = <literal>false</literal>;
+     and rebooting.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     PulseAudio was upgraded to 14.0, with changes to the handling of default sinks.
+     See its <link xlink:href="https://www.freedesktop.org/wiki/Software/PulseAudio/Notes/14.0/">release notes</link>.
+    </para>
+
+    <para>
+     GNOME users may wish to delete their <literal>~/.config/pulse</literal> due to the changes to stream routing
+     logic. See <link xlink:href="https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/832">PulseAudio bug 832</link>
+     for more information.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <package>zookeeper</package> package does not provide
+     <literal>zooInspector.sh</literal> anymore, as that "contrib" has
+     been dropped from upstream releases.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     In the ACME module, the data used to build the hash for the account
+     directory has changed to accomodate new features to reduce account
+     rate limit issues. This will trigger new account creation on the first
+     rebuild following this update. No issues are expected to arise from this,
+     thanks to the new account creation handling.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <xref linkend="opt-users.users._name_.createHome" /> now always ensures home directory permissions to be <literal>0700</literal>.
+     Permissions had previously been ignored for already existing home directories, possibly leaving them readable by others.
+     The option's description was incorrect regarding ownership management and has been simplified greatly.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The GNOME desktop manager once again installs <package>gnome3.epiphany</package> by default.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     NixOS now generates empty <literal>/etc/netgroup</literal>.
+     <literal>/etc/netgroup</literal> defines network-wide groups and may affect to setups using NIS.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Platforms, like <varname>stdenv.hostPlatform</varname>, no longer have a <varname>platform</varname> attribute.
+     It has been (mostly) flattoned away:
+    </para>
+    <itemizedlist>
+     <listitem><para><varname>platform.gcc</varname> is now <varname>gcc</varname></para></listitem>
+     <listitem><para><literal>platform.kernel*</literal> is now <literal>linux-kernel.*</literal></para></listitem>
+    </itemizedlist>
+    <para>
+     Additionally, <varname>platform.kernelArch</varname> moved to the top level as <varname>linuxArch</varname> to match the other <literal>*Arch</literal> variables.
+    </para>
+    <para>
+     The <varname>platform</varname> grouping of these things never meant anything, and was just a historial/implementation artifact that was overdue removal.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <varname>services.restic</varname> now uses a dedicated cache directory for every backup defined in <varname>services.restic.backups</varname>. The old global cache directory, <literal>/root/.cache/restic</literal>, is now unused and can be removed to free up disk space.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <literal>isync</literal>: The <literal>isync</literal> compatibility wrapper was removed and the Master/Slave
+     terminology has been deprecated and should be replaced with Far/Near in the configuration file.
+    </para>
+   </listitem>
+  </itemizedlist>
+ </section>
+</section>
diff --git a/nixos/doc/varlistentry-fixer.rb b/nixos/doc/varlistentry-fixer.rb
index 6c7cc1e6439b1..02168016b554b 100755
--- a/nixos/doc/varlistentry-fixer.rb
+++ b/nixos/doc/varlistentry-fixer.rb
@@ -15,8 +15,8 @@ require "rexml/document"
 include REXML
 
 if ARGV.length < 1 then
-	$stderr.puts "Needs a filename."
-	exit 1
+  $stderr.puts "Needs a filename."
+  exit 1
 end
 
 filename = ARGV.shift
@@ -51,17 +51,17 @@ $touched = false
 # Generates: --optionnamevalue
 #                   ^^  ^^
 doc.elements.each("//varlistentry/term") do |term|
-	["varname", "function", "option", "replaceable"].each do |prev_name|
-		term.elements.each(prev_name) do |el|
-			if el.next_element and
-					el.next_element.name == "replaceable" and
-					el.next_sibling_node.class == Element
-				then
-				$touched = true
-				term.insert_after(el, Text.new(" "))
-			end
-		end
-	end
+  ["varname", "function", "option", "replaceable"].each do |prev_name|
+    term.elements.each(prev_name) do |el|
+      if el.next_element and
+          el.next_element.name == "replaceable" and
+          el.next_sibling_node.class == Element
+        then
+        $touched = true
+        term.insert_after(el, Text.new(" "))
+      end
+    end
+  end
 end
 
 
@@ -75,17 +75,17 @@ end
 # Generates: -Ipath
 #             ^^
 doc.elements.each("//cmdsynopsis/arg") do |term|
-	["option", "replaceable"].each do |prev_name|
-		term.elements.each(prev_name) do |el|
-			if el.next_element and
-				el.next_element.name == "replaceable" and
-				el.next_sibling_node.class == Element
-			then
-				$touched = true
-				term.insert_after(el, Text.new(" "))
-			end
-		end
-	end
+  ["option", "replaceable"].each do |prev_name|
+    term.elements.each(prev_name) do |el|
+      if el.next_element and
+        el.next_element.name == "replaceable" and
+        el.next_sibling_node.class == Element
+      then
+        $touched = true
+        term.insert_after(el, Text.new(" "))
+      end
+    end
+  end
 end
 
 #  <cmdsynopsis>
@@ -104,21 +104,21 @@ end
 # Generates: [{--profile-name | -p }name]
 #                                   ^^^^
 doc.elements.each("//cmdsynopsis/arg") do |term|
-	["group"].each do |prev_name|
-		term.elements.each(prev_name) do |el|
-			if el.next_element and
-				el.next_element.name == "replaceable" and
-				el.next_sibling_node.class == Element
-			then
-				$touched = true
-				term.insert_after(el, Text.new(" "))
-			end
-		end
-	end
+  ["group"].each do |prev_name|
+    term.elements.each(prev_name) do |el|
+      if el.next_element and
+        el.next_element.name == "replaceable" and
+        el.next_sibling_node.class == Element
+      then
+        $touched = true
+        term.insert_after(el, Text.new(" "))
+      end
+    end
+  end
 end
 
 
 if $touched then
-	doc.context[:attribute_quote] = :quote
-	doc.write(output: File.open(filename, "w"))
+  doc.context[:attribute_quote] = :quote
+  doc.write(output: File.open(filename, "w"))
 end
diff --git a/nixos/lib/build-vms.nix b/nixos/lib/build-vms.nix
index b1575fc13bbdf..ebbb0296bef63 100644
--- a/nixos/lib/build-vms.nix
+++ b/nixos/lib/build-vms.nix
@@ -18,9 +18,6 @@ rec {
 
   inherit pkgs;
 
-  qemu = pkgs.qemu_test;
-
-
   # Build a virtual network from an attribute set `{ machine1 =
   # config1; ... machineN = configN; }', where `machineX' is the
   # hostname and `configX' is a NixOS system configuration.  Each
@@ -39,7 +36,6 @@ rec {
         [ ../modules/virtualisation/qemu-vm.nix
           ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs
           { key = "no-manual"; documentation.nixos.enable = false; }
-          { key = "qemu"; system.build.qemu = qemu; }
           { key = "nodes"; _module.args.nodes = nodes; }
         ] ++ optional minimal ../modules/testing/minimal-kernel.nix;
     };
diff --git a/nixos/lib/make-disk-image.nix b/nixos/lib/make-disk-image.nix
index 8aa606a56af80..023d0791a5c7d 100644
--- a/nixos/lib/make-disk-image.nix
+++ b/nixos/lib/make-disk-image.nix
@@ -18,9 +18,13 @@
   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'.
+  # This is a list of attribute sets {source, target, mode, user, group} where
+  # `source' is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target', `mode' is a string containing
+  # the permissions that will be set (ex. "755"), `user' and `group' are the
+  # user and group name that will be set as owner of the files.
+  # `mode', `user', and `group' are optional.
+  # When setting one of `user' or `group', the other needs to be set too.
   contents ? []
 
 , # Type of partition table to use; either "legacy", "efi", or "none".
@@ -28,6 +32,9 @@
   #   partition of reasonable size is created in addition to the root partition.
   # For "legacy", the msdos partition table is used and a single large root
   #   partition is created.
+  # For "legacy+gpt", the GPT partition table is used, a 1MiB no-fs partition for
+  #   use by the bootloader is created, and a single large root 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.
@@ -54,9 +61,14 @@
   format ? "raw"
 }:
 
-assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "hybrid" || partitionTableType == "none";
+assert partitionTableType == "legacy" || partitionTableType == "legacy+gpt" || partitionTableType == "efi" || partitionTableType == "hybrid" || partitionTableType == "none";
 # We use -E offset=X below, which is only supported by e2fsprogs
 assert partitionTableType != "none" -> fsType == "ext4";
+# Either both or none of {user,group} need to be set
+assert lib.all
+         (attrs: ((attrs.user  or null) == null)
+              == ((attrs.group or null) == null))
+         contents;
 
 with lib;
 
@@ -75,6 +87,7 @@ let format' = format; in let
 
   rootPartition = { # switch-case
     legacy = "1";
+    "legacy+gpt" = "2";
     efi = "2";
     hybrid = "3";
   }.${partitionTableType};
@@ -85,6 +98,16 @@ let format' = format; in let
         mklabel msdos \
         mkpart primary ext4 1MiB -1
     '';
+    "legacy+gpt" = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart no-fs 1MB 2MB \
+        set 1 bios_grub on \
+        align-check optimal 1 \
+        mkpart primary ext4 2MB -1 \
+        align-check optimal 2 \
+        print
+    '';
     efi = ''
       parted --script $diskImage -- \
         mklabel gpt \
@@ -120,7 +143,7 @@ let format' = format; in let
 
   binPath = with pkgs; makeBinPath (
     [ rsync
-      utillinux
+      util-linux
       parted
       e2fsprogs
       lkl
@@ -134,6 +157,9 @@ let format' = format; in let
   # !!! should use XML.
   sources = map (x: x.source) contents;
   targets = map (x: x.target) contents;
+  modes   = map (x: x.mode  or "''") contents;
+  users   = map (x: x.user  or "''") contents;
+  groups  = map (x: x.group or "''") contents;
 
   closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; };
 
@@ -160,22 +186,33 @@ let format' = format; in let
     set -f
     sources_=(${concatStringsSep " " sources})
     targets_=(${concatStringsSep " " targets})
+    modes_=(${concatStringsSep " " modes})
     set +f
 
     for ((i = 0; i < ''${#targets_[@]}; i++)); do
       source="''${sources_[$i]}"
       target="''${targets_[$i]}"
+      mode="''${modes_[$i]}"
 
+      if [ -n "$mode" ]; then
+        rsync_chmod_flags="--chmod=$mode"
+      else
+        rsync_chmod_flags=""
+      fi
+      # Unfortunately cptofs only supports modes, not ownership, so we can't use
+      # rsync's --chown option. Instead, we change the ownerships in the
+      # VM script with chown.
+      rsync_flags="-a --no-o --no-g $rsync_chmod_flags"
       if [[ "$source" =~ '*' ]]; then
         # If the source name contains '*', perform globbing.
         mkdir -p $root/$target
         for fn in $source; do
-          rsync -a --no-o --no-g "$fn" $root/$target/
+          rsync $rsync_flags "$fn" $root/$target/
         done
       else
         mkdir -p $root/$(dirname $target)
         if ! [ -e $root/$target ]; then
-          rsync -a --no-o --no-g $source $root/$target
+          rsync $rsync_flags $source $root/$target
         else
           echo "duplicate entry $target -> $source"
           exit 1
@@ -220,12 +257,13 @@ let format' = format; in let
     ''}
 
     echo "copying staging root to image..."
-    cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* /
+    cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* / ||
+      (echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
   '';
 in pkgs.vmTools.runInLinuxVM (
   pkgs.runCommand name
     { preVM = prepareImage;
-      buildInputs = with pkgs; [ utillinux e2fsprogs dosfstools ];
+      buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
       postVM = ''
         ${if format == "raw" then ''
           mv $diskImage $out/${filename}
@@ -270,6 +308,21 @@ in pkgs.vmTools.runInLinuxVM (
       # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
       rm -f $mountPoint/etc/machine-id
 
+      # Set the ownerships of the contents. The modes are set in preVM.
+      # No globbing on targets, so no need to set -f
+      targets_=(${concatStringsSep " " targets})
+      users_=(${concatStringsSep " " users})
+      groups_=(${concatStringsSep " " groups})
+      for ((i = 0; i < ''${#targets_[@]}; i++)); do
+        target="''${targets_[$i]}"
+        user="''${users_[$i]}"
+        group="''${groups_[$i]}"
+        if [ -n "$user$group" ]; then
+          # We have to nixos-enter since we need to use the user and group of the VM
+          nixos-enter --root $mountPoint -- chown -R "$user:$group" "$target"
+        fi
+      done
+
       umount -R /mnt
 
       # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
diff --git a/nixos/lib/make-iso9660-image.nix b/nixos/lib/make-iso9660-image.nix
index 6a0e0e7c635a4..549530965f6e1 100644
--- a/nixos/lib/make-iso9660-image.nix
+++ b/nixos/lib/make-iso9660-image.nix
@@ -48,7 +48,7 @@ assert usbBootable -> isohybridMbrImage != "";
 stdenv.mkDerivation {
   name = isoName;
   builder = ./make-iso9660-image.sh;
-  buildInputs = [ xorriso syslinux zstd libossp_uuid ];
+  nativeBuildInputs = [ xorriso syslinux zstd libossp_uuid ];
 
   inherit isoName bootable bootImage compressImage volumeID efiBootImage efiBootable isohybridMbrImage usbBootable;
 
diff --git a/nixos/lib/make-system-tarball.nix b/nixos/lib/make-system-tarball.nix
index dee91a6ce3f49..dab168f4a4813 100644
--- a/nixos/lib/make-system-tarball.nix
+++ b/nixos/lib/make-system-tarball.nix
@@ -37,7 +37,7 @@ in
 stdenv.mkDerivation {
   name = "tarball";
   builder = ./make-system-tarball.sh;
-  buildInputs = extraInputs;
+  nativeBuildInputs = extraInputs;
 
   inherit fileName extraArgs extraCommands compressCommand;
 
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 156392ad1e320..96b75a49928f0 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -110,7 +110,6 @@ def create_vlan(vlan_nr: str) -> Tuple[str, str, "subprocess.Popen[bytes]", Any]
     pty_master, pty_slave = pty.openpty()
     vde_process = subprocess.Popen(
         ["vde_switch", "-s", vde_socket, "--dirmode", "0700"],
-        bufsize=1,
         stdin=pty_slave,
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
@@ -635,8 +634,7 @@ class Machine:
                 shutil.copy(intermediate, abs_target)
 
     def dump_tty_contents(self, tty: str) -> None:
-        """Debugging: Dump the contents of the TTY<n>
-        """
+        """Debugging: Dump the contents of the TTY<n>"""
         self.execute("fold -w 80 /dev/vcs{} | systemd-cat".format(tty))
 
     def get_screen_text(self) -> str:
@@ -748,7 +746,6 @@ class Machine:
 
         self.process = subprocess.Popen(
             self.script,
-            bufsize=1,
             stdin=subprocess.DEVNULL,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
@@ -862,8 +859,7 @@ class Machine:
         self.send_monitor_command("set_link virtio-net-pci.1 off")
 
     def unblock(self) -> None:
-        """Make the machine reachable.
-        """
+        """Make the machine reachable."""
         self.send_monitor_command("set_link virtio-net-pci.1 on")
 
 
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 498f97336c029..6192be1cd0532 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -3,13 +3,13 @@
   # Use a minimal kernel?
 , minimal ? false
   # Ignored
-, config ? {}
+, config ? { }
   # !!! See comment about args in lib/modules.nix
-, specialArgs ? {}
+, specialArgs ? { }
   # Modules to add to each VM
-, extraConfigurations ? [] }:
+, extraConfigurations ? [ ]
+}:
 
-with import ./build-vms.nix { inherit system pkgs minimal specialArgs extraConfigurations; };
 with pkgs;
 
 rec {
@@ -17,40 +17,41 @@ rec {
   inherit pkgs;
 
 
-  testDriver = let
-    testDriverScript = ./test-driver/test-driver.py;
-  in stdenv.mkDerivation {
-    name = "nixos-test-driver";
-
-    nativeBuildInputs = [ makeWrapper ];
-    buildInputs = [ (python3.withPackages (p: [ p.ptpython ])) ];
-    checkInputs = with python3Packages; [ pylint black mypy ];
-
-    dontUnpack = true;
+  mkTestDriver =
+    let
+      testDriverScript = ./test-driver/test-driver.py;
+    in
+    qemu_pkg: stdenv.mkDerivation {
+      name = "nixos-test-driver";
 
-    preferLocalBuild = true;
+      nativeBuildInputs = [ makeWrapper ];
+      buildInputs = [ (python3.withPackages (p: [ p.ptpython ])) ];
+      checkInputs = with python3Packages; [ pylint black mypy ];
 
-    doCheck = true;
-    checkPhase = ''
-      mypy --disallow-untyped-defs \
-           --no-implicit-optional \
-           --ignore-missing-imports ${testDriverScript}
-      pylint --errors-only ${testDriverScript}
-      black --check --diff ${testDriverScript}
-    '';
+      dontUnpack = true;
 
-    installPhase =
-      ''
-        mkdir -p $out/bin
-        cp ${testDriverScript} $out/bin/nixos-test-driver
-        chmod u+x $out/bin/nixos-test-driver
-        # TODO: copy user script part into this file (append)
+      preferLocalBuild = true;
 
-        wrapProgram $out/bin/nixos-test-driver \
-          --prefix PATH : "${lib.makeBinPath [ qemu_test vde2 netpbm coreutils ]}" \
+      doCheck = true;
+      checkPhase = ''
+        mypy --disallow-untyped-defs \
+             --no-implicit-optional \
+             --ignore-missing-imports ${testDriverScript}
+        pylint --errors-only ${testDriverScript}
+        black --check --diff ${testDriverScript}
       '';
-  };
 
+      installPhase =
+        ''
+          mkdir -p $out/bin
+          cp ${testDriverScript} $out/bin/nixos-test-driver
+          chmod u+x $out/bin/nixos-test-driver
+          # TODO: copy user script part into this file (append)
+
+          wrapProgram $out/bin/nixos-test-driver \
+            --prefix PATH : "${lib.makeBinPath [ qemu_pkg vde2 netpbm coreutils ]}" \
+        '';
+    };
 
   # Run an automated test suite in the given virtual network.
   # `driver' is the script that runs the network.
@@ -66,6 +67,8 @@ rec {
 
           LOGFILE=/dev/null tests='exec(os.environ["testScript"])' ${driver}/bin/nixos-test-driver
         '';
+
+      passthru = driver.passthru;
     };
 
 
@@ -73,11 +76,11 @@ rec {
     { testScript
     , enableOCR ? false
     , name ? "unnamed"
-    # Skip linting (mainly intended for faster dev cycles)
+      # Skip linting (mainly intended for faster dev cycles)
     , skipLint ? false
+    , passthru ? {}
     , ...
     } @ t:
-
     let
       # A standard store path to the vm monitor is built like this:
       #   /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor
@@ -86,25 +89,7 @@ rec {
       maxTestNameLen = 50;
       testNameLen = builtins.stringLength name;
 
-      testDriverName = with builtins;
-        if testNameLen > maxTestNameLen then
-          abort ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " +
-            "it's currently ${toString testNameLen} characters long.")
-        else
-          "nixos-test-driver-${name}";
 
-      nodes = buildVirtualNetwork (
-        t.nodes or (if t ? machine then { machine = t.machine; } else { }));
-
-      testScript' =
-        # Call the test script with the computed nodes.
-        if lib.isFunction testScript
-        then testScript { inherit nodes; }
-        else testScript;
-
-      vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
-
-      vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
 
       ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; };
 
@@ -113,72 +98,124 @@ rec {
       # 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
-        { buildInputs = [ makeWrapper];
-          testScript = testScript';
-          preferLocalBuild = true;
-          testName = name;
-        }
-        ''
-          mkdir -p $out/bin
-
-          echo -n "$testScript" > $out/test-script
-          ${lib.optionalString (!skipLint) ''
-            ${python3Packages.black}/bin/black --check --diff $out/test-script
-          ''}
-
-          ln -s ${testDriver}/bin/nixos-test-driver $out/bin/
-          vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
-          wrapProgram $out/bin/nixos-test-driver \
-            --add-flags "''${vms[*]}" \
-            ${lib.optionalString enableOCR
-              "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
-            --run "export testScript=\"\$(${coreutils}/bin/cat $out/test-script)\"" \
-            --set VLANS '${toString vlans}'
-          ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
-          wrapProgram $out/bin/nixos-run-vms \
-            --add-flags "''${vms[*]}" \
-            ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \
-            --set tests 'start_all(); join_all();' \
-            --set VLANS '${toString vlans}' \
-            ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"}
-        ''); # "
+      mkDriver = qemu_pkg:
+        let
+          build-vms = import ./build-vms.nix {
+            inherit system pkgs minimal specialArgs;
+            extraConfigurations = extraConfigurations ++ (pkgs.lib.optional (qemu_pkg != null)
+              {
+                virtualisation.qemu.package = qemu_pkg;
+              }
+            );
+          };
+
+          # FIXME: get this pkg from the module system
+          testDriver = mkTestDriver (if qemu_pkg == null then pkgs.qemu_test else qemu_pkg);
+
+          nodes = build-vms.buildVirtualNetwork (
+            t.nodes or (if t ? machine then { machine = t.machine; } else { })
+          );
+          vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
+          vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
+
+          testScript' =
+            # Call the test script with the computed nodes.
+            if lib.isFunction testScript
+            then testScript { inherit nodes; }
+            else testScript;
+
+          testDriverName = with builtins;
+            if testNameLen > maxTestNameLen then
+              abort
+                ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " +
+                  "it's currently ${toString testNameLen} characters long.")
+            else
+              "nixos-test-driver-${name}";
+
+          warn = if skipLint then lib.warn "Linting is disabled!" else lib.id;
+        in
+        warn (runCommand testDriverName
+          {
+            buildInputs = [ makeWrapper ];
+            testScript = testScript';
+            preferLocalBuild = true;
+            testName = name;
+            passthru = passthru // {
+              inherit nodes;
+            };
+          }
+          ''
+            mkdir -p $out/bin
+
+            echo -n "$testScript" > $out/test-script
+            ${lib.optionalString (!skipLint) ''
+              ${python3Packages.black}/bin/black --check --diff $out/test-script
+            ''}
+
+            ln -s ${testDriver}/bin/nixos-test-driver $out/bin/
+            vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
+            wrapProgram $out/bin/nixos-test-driver \
+              --add-flags "''${vms[*]}" \
+              ${lib.optionalString enableOCR
+                "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
+              --run "export testScript=\"\$(${coreutils}/bin/cat $out/test-script)\"" \
+              --set VLANS '${toString vlans}'
+            ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
+            wrapProgram $out/bin/nixos-run-vms \
+              --add-flags "''${vms[*]}" \
+              ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \
+              --set tests 'start_all(); join_all();' \
+              --set VLANS '${toString vlans}' \
+              ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"}
+          ''); # "
 
       passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
-        meta = (drv.meta or {}) // t.meta;
+        meta = (drv.meta or { }) // t.meta;
       };
 
+      driver = mkDriver null;
+      driverInteractive = mkDriver pkgs.qemu;
+
       test = passMeta (runTests driver);
 
-      nodeNames = builtins.attrNames nodes;
+      nodeNames = builtins.attrNames driver.nodes;
       invalidNodeNames = lib.filter
-        (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null) nodeNames;
+        (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null)
+        nodeNames;
 
     in
-      if lib.length invalidNodeNames > 0 then
-        throw ''
-          Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
-          All machines are referenced as python variables in the testing framework which will break the
-          script when special characters are used.
+    if lib.length invalidNodeNames > 0 then
+      throw ''
+        Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
+        All machines are referenced as python variables in the testing framework which will break the
+        script when special characters are used.
 
-          Please stick to alphanumeric chars and underscores as separation.
-        ''
-      else
-        test // {
-          inherit nodes driver test;
-        };
+        Please stick to alphanumeric chars and underscores as separation.
+      ''
+    else
+      test // {
+        inherit test driver driverInteractive;
+        inherit (driver) nodes;
+      };
 
   runInMachine =
     { drv
     , machine
     , preBuild ? ""
     , postBuild ? ""
+    , qemu ? pkgs.qemu_test
     , ... # ???
     }:
     let
-      vm = buildVM { }
-        [ machine
-          { key = "run-in-machine";
+      build-vms = import ./build-vms.nix {
+        inherit system pkgs minimal specialArgs extraConfigurations;
+      };
+
+      vm = build-vms.buildVM { }
+        [
+          machine
+          {
+            key = "run-in-machine";
             networking.hostName = "client";
             nix.readOnlyStore = false;
             virtualisation.writableStore = false;
@@ -221,20 +258,20 @@ rec {
         unset xchg
 
         export tests='${testScript}'
-        ${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm
+        ${mkTestDriver qemu}/bin/nixos-test-driver --keep-vm-state ${vm.config.system.build.vm}/bin/run-*-vm
       ''; # */
 
     in
-      lib.overrideDerivation drv (attrs: {
-        requiredSystemFeatures = [ "kvm" ];
-        builder = "${bash}/bin/sh";
-        args = ["-e" vmRunCommand];
-        origArgs = attrs.args;
-        origBuilder = attrs.builder;
-      });
+    lib.overrideDerivation drv (attrs: {
+      requiredSystemFeatures = [ "kvm" ];
+      builder = "${bash}/bin/sh";
+      args = [ "-e" vmRunCommand ];
+      origArgs = attrs.args;
+      origBuilder = attrs.builder;
+    });
 
 
-  runInMachineWithX = { require ? [], ... } @ args:
+  runInMachineWithX = { require ? [ ], ... } @ args:
     let
       client =
         { ... }:
@@ -250,13 +287,13 @@ rec {
           services.xserver.windowManager.icewm.enable = true;
         };
     in
-      runInMachine ({
-        machine = client;
-        preBuild =
-          ''
-            client.wait_for_x()
-          '';
-      } // args);
+    runInMachine ({
+      machine = client;
+      preBuild =
+        ''
+          client.wait_for_x()
+        '';
+    } // args);
 
 
   simpleTest = as: (makeTest as).test;
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index b09f4ca47a3fc..0ecf07669a119 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -57,7 +57,7 @@ in {
     inherit (cfg) contents format name;
     pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
     partitionTableType = if config.ec2.efi then "efi"
-                         else if config.ec2.hvm then "legacy"
+                         else if config.ec2.hvm then "legacy+gpt"
                          else "none";
     diskSize = cfg.sizeMB;
     fsType = "ext4";
diff --git a/nixos/maintainers/scripts/ec2/create-amis.sh b/nixos/maintainers/scripts/ec2/create-amis.sh
index 89e24f2ccfd95..691d7fcfcba44 100755
--- a/nixos/maintainers/scripts/ec2/create-amis.sh
+++ b/nixos/maintainers/scripts/ec2/create-amis.sh
@@ -1,13 +1,15 @@
 #!/usr/bin/env nix-shell
 #!nix-shell -p awscli -p jq -p qemu -i bash
+# shellcheck shell=bash
 
 # Uploads and registers NixOS images built from the
 # <nixos/release.nix> amazonImage attribute. Images are uploaded and
 # registered via a home region, and then copied to other regions.
 
-# The home region requires an s3 bucket, and a "vmimport" IAM role
-# with access to the S3 bucket.  Configuration of the vmimport role is
-# documented in
+# The home region requires an s3 bucket, and an IAM role named "vmimport"
+# (by default) with access to the S3 bucket. The name can be
+# configured with the "service_role_name" variable. Configuration of the
+# vmimport role is documented in
 # https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html
 
 # set -x
@@ -17,6 +19,7 @@ set -euo pipefail
 state_dir=$HOME/amis/ec2-images
 home_region=eu-west-1
 bucket=nixos-amis
+service_role_name=vmimport
 
 regions=(eu-west-1 eu-west-2 eu-west-3 eu-central-1 eu-north-1
          us-east-1 us-east-2 us-west-1 us-west-2
@@ -64,7 +67,7 @@ image_logical_bytes=$(read_image_info .logical_bytes)
 
 # Derived attributes
 
-image_logical_gigabytes=$((($image_logical_bytes-1)/1024/1024/1024+1)) # Round to the next GB
+image_logical_gigabytes=$(((image_logical_bytes-1)/1024/1024/1024+1)) # Round to the next GB
 
 case "$image_system" in
     aarch64-linux)
@@ -100,7 +103,7 @@ write_state() {
     local type=$2
     local val=$3
 
-    mkdir -p $state_dir
+    mkdir -p "$state_dir"
     echo "$val" > "$state_dir/$state_key.$type"
 }
 
@@ -110,8 +113,8 @@ wait_for_import() {
     local state snapshot_id
     log "Waiting for import task $task_id to be completed"
     while true; do
-        read state progress snapshot_id < <(
-            aws ec2 describe-import-snapshot-tasks --region $region --import-task-ids "$task_id" | \
+        read -r state progress snapshot_id < <(
+            aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" | \
                 jq -r '.ImportSnapshotTasks[].SnapshotTaskDetail | "\(.Status) \(.Progress) \(.SnapshotId)"'
         )
         log " ... state=$state progress=$progress snapshot_id=$snapshot_id"
@@ -125,6 +128,8 @@ wait_for_import() {
                 ;;
             *)
                 log "Unexpected snapshot import state: '${state}'"
+                log "Full response: "
+                aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" >&2
                 exit 1
                 ;;
         esac
@@ -138,8 +143,8 @@ wait_for_image() {
     log "Waiting for image $ami_id to be available"
 
     while true; do
-        read state < <(
-            aws ec2 describe-images --image-ids "$ami_id" --region $region | \
+        read -r state < <(
+            aws ec2 describe-images --image-ids "$ami_id" --region "$region" | \
                 jq -r ".Images[].State"
         )
         log " ... state=$state"
@@ -163,7 +168,7 @@ make_image_public() {
     local region=$1
     local ami_id=$2
 
-    wait_for_image $region "$ami_id"
+    wait_for_image "$region" "$ami_id"
 
     log "Making image $ami_id public"
 
@@ -177,27 +182,30 @@ upload_image() {
     local aws_path=${image_file#/}
 
     local state_key="$region.$image_label.$image_system"
-    local task_id=$(read_state "$state_key" task_id)
-    local snapshot_id=$(read_state "$state_key" snapshot_id)
-    local ami_id=$(read_state "$state_key" ami_id)
+    local task_id
+    task_id=$(read_state "$state_key" task_id)
+    local snapshot_id
+    snapshot_id=$(read_state "$state_key" snapshot_id)
+    local ami_id
+    ami_id=$(read_state "$state_key" ami_id)
 
     if [ -z "$task_id" ]; then
         log "Checking for image on S3"
         if ! aws s3 ls --region "$region" "s3://${bucket}/${aws_path}" >&2; then
             log "Image missing from aws, uploading"
-            aws s3 cp --region $region "$image_file" "s3://${bucket}/${aws_path}" >&2
+            aws s3 cp --region "$region" "$image_file" "s3://${bucket}/${aws_path}" >&2
         fi
 
         log "Importing image from S3 path s3://$bucket/$aws_path"
 
-        task_id=$(aws ec2 import-snapshot --disk-container "{
+        task_id=$(aws ec2 import-snapshot --role-name "$service_role_name" --disk-container "{
           \"Description\": \"nixos-image-${image_label}-${image_system}\",
           \"Format\": \"vhd\",
           \"UserBucket\": {
               \"S3Bucket\": \"$bucket\",
               \"S3Key\": \"$aws_path\"
           }
-        }" --region $region | jq -r '.ImportTaskId')
+        }" --region "$region" | jq -r '.ImportTaskId')
 
         write_state "$state_key" task_id "$task_id"
     fi
@@ -211,7 +219,7 @@ upload_image() {
         log "Registering snapshot $snapshot_id as AMI"
 
         local block_device_mappings=(
-            "DeviceName=/dev/xvda,Ebs={SnapshotId=$snapshot_id,VolumeSize=$image_logical_gigabytes,DeleteOnTermination=true,VolumeType=gp2}"
+            "DeviceName=/dev/xvda,Ebs={SnapshotId=$snapshot_id,VolumeSize=$image_logical_gigabytes,DeleteOnTermination=true,VolumeType=gp3}"
         )
 
         local extra_flags=(
@@ -221,16 +229,16 @@ upload_image() {
             --virtualization-type hvm
         )
 
-        block_device_mappings+=(DeviceName=/dev/sdb,VirtualName=ephemeral0)
-        block_device_mappings+=(DeviceName=/dev/sdc,VirtualName=ephemeral1)
-        block_device_mappings+=(DeviceName=/dev/sdd,VirtualName=ephemeral2)
-        block_device_mappings+=(DeviceName=/dev/sde,VirtualName=ephemeral3)
+        block_device_mappings+=("DeviceName=/dev/sdb,VirtualName=ephemeral0")
+        block_device_mappings+=("DeviceName=/dev/sdc,VirtualName=ephemeral1")
+        block_device_mappings+=("DeviceName=/dev/sdd,VirtualName=ephemeral2")
+        block_device_mappings+=("DeviceName=/dev/sde,VirtualName=ephemeral3")
 
         ami_id=$(
             aws ec2 register-image \
                 --name "$image_name" \
                 --description "$image_description" \
-                --region $region \
+                --region "$region" \
                 --architecture $amazon_arch \
                 --block-device-mappings "${block_device_mappings[@]}" \
                 "${extra_flags[@]}" \
@@ -240,7 +248,7 @@ upload_image() {
         write_state "$state_key" ami_id "$ami_id"
     fi
 
-    make_image_public $region "$ami_id"
+    make_image_public "$region" "$ami_id"
 
     echo "$ami_id"
 }
@@ -268,7 +276,7 @@ copy_to_region() {
         write_state "$state_key" ami_id "$ami_id"
     fi
 
-    make_image_public $region "$ami_id"
+    make_image_public "$region" "$ami_id"
 
     echo "$ami_id"
 }
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
index f662ed62d31dc..1339227f1e022 100644
--- a/nixos/modules/config/console.nix
+++ b/nixos/modules/config/console.nix
@@ -83,27 +83,13 @@ in
     packages = mkOption {
       type = types.listOf types.package;
       default = with pkgs.kbdKeymaps; [ dvp neo ];
-      defaultText = ''with pkgs.kbdKeymaps; [ dvp neo ]'';
+      defaultText = "with pkgs.kbdKeymaps; [ dvp neo ]";
       description = ''
         List of additional packages that provide console fonts, keymaps and
         other resources for virtual consoles use.
       '';
     };
 
-    extraTTYs = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      example = ["tty8" "tty9"];
-      description = ''
-        TTY (virtual console) devices, in addition to the consoles on
-        which mingetty and syslogd run, that must be initialised.
-        Only useful if you have some program that you want to run on
-        some fixed console.  For example, the NixOS installation CD
-        opens the manual in a web browser on console 7, so it sets
-        <option>console.extraTTYs</option> to <literal>["tty7"]</literal>.
-      '';
-    };
-
     useXkbConfig = mkOption {
       type = types.bool;
       default = false;
@@ -159,7 +145,8 @@ in
         '';
 
         systemd.services.systemd-vconsole-setup =
-          { before = [ "display-manager.service" ];
+          {
+            before = optional config.services.xserver.enable "display-manager.service";
             after = [ "systemd-udev-settle.service" ];
             restartTriggers = [ vconsoleConf consoleEnv ];
           };
@@ -199,5 +186,9 @@ in
     (mkRenamedOptionModule [ "i18n" "consoleUseXkbConfig" ] [ "console" "useXkbConfig" ])
     (mkRenamedOptionModule [ "boot" "earlyVconsoleSetup" ] [ "console" "earlySetup" ])
     (mkRenamedOptionModule [ "boot" "extraTTYs" ] [ "console" "extraTTYs" ])
+    (mkRemovedOptionModule [ "console" "extraTTYs" ] ''
+      Since NixOS switched to systemd (circa 2012), TTYs have been spawned on
+      demand, so there is no need to configure them manually.
+    '')
   ];
 }
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index 5b681ca59464d..6e7b8c4b88a20 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -436,7 +436,7 @@ in
         useEmbeddedBitmaps = mkOption {
           type = types.bool;
           default = false;
-          description = ''Use embedded bitmaps in fonts like Calibri.'';
+          description = "Use embedded bitmaps in fonts like Calibri.";
         };
 
       };
diff --git a/nixos/modules/config/fonts/fontdir.nix b/nixos/modules/config/fonts/fontdir.nix
index 264d73ebafa53..c4bd3a077d339 100644
--- a/nixos/modules/config/fonts/fontdir.nix
+++ b/nixos/modules/config/fonts/fontdir.nix
@@ -8,7 +8,7 @@ let
 
   x11Fonts = pkgs.runCommand "X11-fonts" { preferLocalBuild = true; } ''
     mkdir -p "$out/share/X11/fonts"
-    font_regexp='.*\.\(ttf\|otf\|pcf\|pfa\|pfb\|bdf\)\(\.gz\)?'
+    font_regexp='.*\.\(ttf\|ttc\|otf\|pcf\|pfa\|pfb\|bdf\)\(\.gz\)?'
     find ${toString config.fonts.fonts} -regex "$font_regexp" \
       -exec ln -sf -t "$out/share/X11/fonts" '{}' \;
     cd "$out/share/X11/fonts"
diff --git a/nixos/modules/config/gnu.nix b/nixos/modules/config/gnu.nix
index 93d1309701902..255d9741ba714 100644
--- a/nixos/modules/config/gnu.nix
+++ b/nixos/modules/config/gnu.nix
@@ -1,11 +1,9 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 {
   options = {
-    gnu = mkOption {
-      type = types.bool;
+    gnu = lib.mkOption {
+      type = lib.types.bool;
       default = false;
       description = ''
         When enabled, GNU software is chosen by default whenever a there is
@@ -15,7 +13,7 @@ with lib;
     };
   };
 
-  config = mkIf config.gnu {
+  config = lib.mkIf config.gnu {
 
     environment.systemPackages = with pkgs;
       # TODO: Adjust `requiredPackages' from `system-path.nix'.
@@ -26,7 +24,7 @@ with lib;
         nano zile
         texinfo # for the stand-alone Info reader
       ]
-      ++ stdenv.lib.optional (!stdenv.isAarch32) grub2;
+      ++ lib.optional (!stdenv.isAarch32) grub2;
 
 
     # GNU GRUB, where available.
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
index feb76581a720f..991b449d80b55 100644
--- a/nixos/modules/config/i18n.nix
+++ b/nixos/modules/config/i18n.nix
@@ -84,7 +84,7 @@ with lib;
     environment.etc."locale.conf".source = pkgs.writeText "locale.conf"
       ''
         LANG=${config.i18n.defaultLocale}
-        ${concatStringsSep "\n" (mapAttrsToList (n: v: ''${n}=${v}'') config.i18n.extraLocaleSettings)}
+        ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n}=${v}") config.i18n.extraLocaleSettings)}
       '';
 
   };
diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix
index 1a5dbcd4e26ba..35813c168fd85 100644
--- a/nixos/modules/config/ldap.nix
+++ b/nixos/modules/config/ldap.nix
@@ -59,30 +59,28 @@ in
 
     users.ldap = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable authentication against an LDAP server.";
-      };
+      enable = mkEnableOption "authentication against an LDAP server";
 
       loginPam = mkOption {
         type = types.bool;
         default = true;
-        description = "Whether to include authentication against LDAP in login PAM";
+        description = "Whether to include authentication against LDAP in login PAM.";
       };
 
       nsswitch = mkOption {
         type = types.bool;
         default = true;
-        description = "Whether to include lookup against LDAP in NSS";
+        description = "Whether to include lookup against LDAP in NSS.";
       };
 
       server = mkOption {
+        type = types.str;
         example = "ldap://ldap.example.org/";
         description = "The URL of the LDAP server.";
       };
 
       base = mkOption {
+        type = types.str;
         example = "dc=example,dc=org";
         description = "The distinguished name of the search base.";
       };
@@ -129,7 +127,7 @@ in
           type = types.lines;
           description = ''
             Extra configuration options that will be added verbatim at
-            the end of the nslcd configuration file (nslcd.conf).
+            the end of the nslcd configuration file (<literal>nslcd.conf(5)</literal>).
           '' ;
         } ;
 
@@ -180,7 +178,7 @@ in
           description = ''
             Specifies the time limit (in seconds) to use when connecting
             to the directory server. This is distinct from the time limit
-            specified in <literal>users.ldap.timeLimit</literal> and affects
+            specified in <option>users.ldap.timeLimit</option> and affects
             the initial server connection only.
           '';
         };
@@ -197,7 +195,7 @@ in
             actually contact the directory server, and it is possible that
             a malformed configuration file will trigger reconnection. If
             <literal>soft</literal> is specified, then
-            <literal>nss_ldap</literal> will return immediately on server
+            <package>nss_ldap</package> will return immediately on server
             failure. All hard reconnect policies block with exponential
             backoff before retrying.
           '';
@@ -209,10 +207,10 @@ in
         type = types.lines;
         description = ''
           Extra configuration options that will be added verbatim at
-          the end of the ldap configuration file (ldap.conf).
-          If <literal>users.ldap.daemon</literal> is enabled, this
+          the end of the ldap configuration file (<literal>ldap.conf(5)</literal>).
+          If <option>users.ldap.daemon</option> is enabled, this
           configuration will not be used. In that case, use
-          <literal>users.ldap.daemon.extraConfig</literal> instead.
+          <option>users.ldap.daemon.extraConfig</option> instead.
         '' ;
       };
 
@@ -240,9 +238,9 @@ in
       '';
     };
 
-    system.nssModules = singleton (
+    system.nssModules = mkIf cfg.nsswitch (singleton (
       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";
@@ -276,7 +274,12 @@ in
           } >"$conf"
           mv -fT "$conf" /run/nslcd/nslcd.conf
         '';
-        restartTriggers = [ "/run/nslcd/nslcd.conf" ];
+
+        restartTriggers = [
+          nslcdConfig
+          cfg.bind.passwordFile
+          cfg.daemon.rootpwmodpwFile
+        ];
 
         serviceConfig = {
           ExecStart = "${nslcdWrapped}/bin/nslcd";
diff --git a/nixos/modules/config/malloc.nix b/nixos/modules/config/malloc.nix
index 31a659ee83fe9..a3eb55d8a42e8 100644
--- a/nixos/modules/config/malloc.nix
+++ b/nixos/modules/config/malloc.nix
@@ -23,7 +23,7 @@ let
     };
 
     scudo = {
-      libPath = "${pkgs.llvmPackages.compiler-rt}/lib/linux/libclang_rt.scudo-x86_64.so";
+      libPath = "${pkgs.llvmPackages_latest.compiler-rt}/lib/linux/libclang_rt.scudo-x86_64.so";
       description = ''
         A user-mode allocator based on LLVM Sanitizer’s CombinedAllocator,
         which aims at providing additional mitigations against heap based
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index 4cb7d81c99729..dba8977e482c1 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -58,6 +58,7 @@ in
         "2.nixos.pool.ntp.org"
         "3.nixos.pool.ntp.org"
       ];
+      type = types.listOf types.str;
       description = ''
         The set of NTP servers from which to synchronise.
       '';
@@ -193,6 +194,9 @@ in
           cat ${escapeShellArgs cfg.hostFiles} > $out
         '';
 
+        # /etc/netgroup: Network-wide groups.
+        netgroup.text = mkDefault "";
+
         # /etc/host.conf: resolver configuration file
         "host.conf".text = ''
           multi on
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index 941ab78f86321..14fe180d0bc5f 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -29,12 +29,14 @@ with lib;
     nixpkgs.overlays = singleton (const (super: {
       cairo = super.cairo.override { x11Support = false; };
       dbus = super.dbus.override { x11Support = false; };
+      beam = super.beam_nox;
       networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; };
+      networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
       networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; };
       networkmanager-openconnect = super.networkmanager-openconnect.override { withGnome = false; };
       networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; };
+      networkmanager-sstp = super.networkmanager-vpnc.override { withGnome = false; };
       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/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index 408d0a9c33f27..c0e90a8c26e6c 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -36,6 +36,8 @@ let
         ${addModuleIf cfg.zeroconf.discovery.enable "module-zeroconf-discover"}
         ${addModuleIf cfg.tcp.enable (concatStringsSep " "
            ([ "module-native-protocol-tcp" ] ++ allAnon ++ ipAnon))}
+        ${addModuleIf config.services.jack.jackd.enable "module-jack-sink"}
+        ${addModuleIf config.services.jack.jackd.enable "module-jack-source"}
         ${cfg.extraConfig}
       '';
     };
@@ -144,7 +146,9 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.pulseaudio;
+        default = if config.services.jack.jackd.enable
+                  then pkgs.pulseaudioFull
+                  else pkgs.pulseaudio;
         defaultText = "pkgs.pulseaudio";
         example = literalExample "pkgs.pulseaudioFull";
         description = ''
@@ -179,7 +183,7 @@ in {
         config = mkOption {
           type = types.attrsOf types.unspecified;
           default = {};
-          description = ''Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.'';
+          description = "Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.";
           example = literalExample ''{ realtime-scheduling = "yes"; }'';
         };
       };
@@ -259,7 +263,7 @@ in {
           (drv: drv.override { pulseaudio = overriddenPackage; })
           cfg.extraModules;
         modulePaths = builtins.map
-          (drv: "${drv}/lib/pulse-${overriddenPackage.version}/modules")
+          (drv: "${drv}/${overriddenPackage.pulseDir}/modules")
           # User-provided extra modules take precedence
           (overriddenModules ++ [ overriddenPackage ]);
       in lib.concatStringsSep ":" modulePaths;
@@ -284,6 +288,8 @@ in {
             RestartSec = "500ms";
             PassEnvironment = "DISPLAY";
           };
+        } // optionalAttrs config.services.jack.jackd.enable {
+          environment.JACK_PROMISCUOUS_SERVER = "jackaudio";
         };
         sockets.pulseaudio = {
           wantedBy = [ "sockets.target" ];
diff --git a/nixos/modules/config/swap.nix b/nixos/modules/config/swap.nix
index adb4e2294213d..59bc9e9d11e00 100644
--- a/nixos/modules/config/swap.nix
+++ b/nixos/modules/config/swap.nix
@@ -185,9 +185,7 @@ in
           { description = "Initialisation of swap device ${sw.device}";
             wantedBy = [ "${realDevice'}.swap" ];
             before = [ "${realDevice'}.swap" ];
-            # If swap is encrypted, depending on rngd resolves a possible entropy starvation during boot
-            after = mkIf (config.security.rngd.enable && sw.randomEncryption.enable) [ "rngd.service" ];
-            path = [ pkgs.utillinux ] ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
+            path = [ pkgs.util-linux ] ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
 
             script =
               ''
diff --git a/nixos/modules/config/system-path.nix b/nixos/modules/config/system-path.nix
index 67305e8499cb5..aee7a041d043e 100644
--- a/nixos/modules/config/system-path.nix
+++ b/nixos/modules/config/system-path.nix
@@ -33,10 +33,11 @@ let
       pkgs.ncurses
       pkgs.netcat
       config.programs.ssh.package
+      pkgs.mkpasswd
       pkgs.procps
       pkgs.su
       pkgs.time
-      pkgs.utillinux
+      pkgs.util-linux
       pkgs.which
       pkgs.zstd
     ];
@@ -142,6 +143,8 @@ in
         "/share/kservices5"
         "/share/kservicetypes5"
         "/share/kxmlgui5"
+        "/share/systemd"
+        "/share/thumbnailers"
       ];
 
     system.path = pkgs.buildEnv {
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index e220aa6109081..44040217b0271 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -16,8 +16,7 @@ my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
 
 sub updateFile {
     my ($path, $contents, $perms) = @_;
-    write_file("$path.tmp", { binmode => ':utf8', perms => $perms // 0644 }, $contents);
-    rename("$path.tmp", $path) or die;
+    write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
 }
 
 
@@ -98,7 +97,7 @@ sub parseGroup {
     return ($f[0], { name => $f[0], password => $f[1], gid => $gid, members => $f[3] });
 }
 
-my %groupsCur = -f "/etc/group" ? map { parseGroup } read_file("/etc/group") : ();
+my %groupsCur = -f "/etc/group" ? map { parseGroup } read_file("/etc/group", { binmode => ":utf8" }) : ();
 
 # Read the current /etc/passwd.
 sub parseUser {
@@ -109,20 +108,19 @@ sub parseUser {
     return ($f[0], { name => $f[0], fakePassword => $f[1], uid => $uid,
         gid => $f[3], description => $f[4], home => $f[5], shell => $f[6] });
 }
-
-my %usersCur = -f "/etc/passwd" ? map { parseUser } read_file("/etc/passwd") : ();
+my %usersCur = -f "/etc/passwd" ? map { parseUser } read_file("/etc/passwd", { binmode => ":utf8" }) : ();
 
 # Read the groups that were created declaratively (i.e. not by groups)
 # in the past. These must be removed if they are no longer in the
 # current spec.
 my $declGroupsFile = "/var/lib/nixos/declarative-groups";
 my %declGroups;
-$declGroups{$_} = 1 foreach split / /, -e $declGroupsFile ? read_file($declGroupsFile) : "";
+$declGroups{$_} = 1 foreach split / /, -e $declGroupsFile ? read_file($declGroupsFile, { binmode => ":utf8" }) : "";
 
 # Idem for the users.
 my $declUsersFile = "/var/lib/nixos/declarative-users";
 my %declUsers;
-$declUsers{$_} = 1 foreach split / /, -e $declUsersFile ? read_file($declUsersFile) : "";
+$declUsers{$_} = 1 foreach split / /, -e $declUsersFile ? read_file($declUsersFile, { binmode => ":utf8" }) : "";
 
 
 # Generate a new /etc/group containing the declared groups.
@@ -175,7 +173,7 @@ foreach my $name (keys %groupsCur) {
 # Rewrite /etc/group. FIXME: acquire lock.
 my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" }
     (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
-updateFile($gidMapFile, encode_json($gidMap));
+updateFile($gidMapFile, to_json($gidMap));
 updateFile("/etc/group", \@lines);
 system("nscd --invalidate group");
 
@@ -211,10 +209,11 @@ foreach my $u (@{$spec->{users}}) {
         }
     }
 
-    # Create a home directory.
+    # Ensure home directory incl. ownership and permissions.
     if ($u->{createHome}) {
         make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home};
         chown $u->{uid}, $u->{gid}, $u->{home};
+        chmod 0700, $u->{home};
     }
 
     if (defined $u->{passwordFile}) {
@@ -228,6 +227,15 @@ foreach my $u (@{$spec->{users}}) {
         $u->{hashedPassword} = hashPassword($u->{password});
     }
 
+    if (!defined $u->{shell}) {
+        if (defined $existing) {
+            $u->{shell} = $existing->{shell};
+        } else {
+            warn "warning: no declarative or previous shell for ‘$name’, setting shell to nologin\n";
+            $u->{shell} = "/run/current-system/sw/bin/nologin";
+        }
+    }
+
     $u->{fakePassword} = $existing->{fakePassword} // "x";
     $usersOut{$name} = $u;
 
@@ -251,7 +259,7 @@ foreach my $name (keys %usersCur) {
 # Rewrite /etc/passwd. FIXME: acquire lock.
 @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" }
     (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
-updateFile($uidMapFile, encode_json($uidMap));
+updateFile($uidMapFile, to_json($uidMap));
 updateFile("/etc/passwd", \@lines);
 system("nscd --invalidate passwd");
 
@@ -260,7 +268,7 @@ system("nscd --invalidate passwd");
 my @shadowNew;
 my %shadowSeen;
 
-foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow") : ()) {
+foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow", { binmode => ":utf8" }) : ()) {
     chomp $line;
     my ($name, $hashedPassword, @rest) = split(':', $line, -9);
     my $u = $usersOut{$name};;
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 1bb1317a8e85a..1a530b9f01359 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -35,8 +35,7 @@ let
   '';
 
   hashedPasswordDescription = ''
-    To generate a hashed password install the <literal>mkpasswd</literal>
-    package and run <literal>mkpasswd -m sha-512</literal>.
+    To generate a hashed password 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
@@ -139,8 +138,22 @@ let
         '';
       };
 
+      pamMount = mkOption {
+        type = with types; attrsOf str;
+        default = {};
+        description = ''
+          Attributes for user's entry in
+          <filename>pam_mount.conf.xml</filename>.
+          Useful attributes might include <code>path</code>,
+          <code>options</code>, <code>fstype</code>, and <code>server</code>.
+          See <link
+          xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />
+          for more information.
+        '';
+      };
+
       shell = mkOption {
-        type = types.either types.shellPackage types.path;
+        type = types.nullOr (types.either types.shellPackage types.path);
         default = pkgs.shadow;
         defaultText = "pkgs.shadow";
         example = literalExample "pkgs.bashInteractive";
@@ -185,10 +198,8 @@ let
         type = types.bool;
         default = false;
         description = ''
-          If true, the home directory will be created automatically. If this
-          option is true and the home directory already exists but is not
-          owned by the user, directory owner and group will be changed to
-          match the user.
+          Whether to create the home directory and ensure ownership as well as
+          permissions to match the user.
         '';
       };
 
@@ -353,7 +364,7 @@ let
       count = mkOption {
         type = types.int;
         default = 1;
-        description = ''Count of subordinate user ids'';
+        description = "Count of subordinate user ids";
       };
     };
   };
@@ -370,7 +381,7 @@ let
       count = mkOption {
         type = types.int;
         default = 1;
-        description = ''Count of subordinate group ids'';
+        description = "Count of subordinate group ids";
       };
     };
   };
@@ -557,7 +568,7 @@ in {
     # Install all the user shells
     environment.systemPackages = systemShells;
 
-    environment.etc = (mapAttrs' (name: { packages, ... }: {
+    environment.etc = (mapAttrs' (_: { packages, name, ... }: {
       name = "profiles/per-user/${name}";
       value.source = pkgs.buildEnv {
         name = "user-environment";
diff --git a/nixos/modules/config/xdg/portal.nix b/nixos/modules/config/xdg/portal.nix
index 3c7cd729c60a6..80ec3126ca546 100644
--- a/nixos/modules/config/xdg/portal.nix
+++ b/nixos/modules/config/xdg/portal.nix
@@ -62,7 +62,7 @@ with lib;
       services.dbus.packages  = packages;
       systemd.packages = packages;
 
-      environment.variables = {
+      environment.sessionVariables = {
         GTK_USE_PORTAL = mkIf cfg.gtkUsePortal "1";
         XDG_DESKTOP_PORTAL_DIR = "${joinedPortals}/share/xdg-desktop-portal/portals";
       };
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 5e9870bf6b1cf..1f513b7e4daef 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -80,6 +80,15 @@ in
         '';
       };
 
+      memoryMax = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        description = ''
+          Maximum total amount of memory (in bytes) that can be used by the zram
+          swap devices.
+        '';
+      };
+
       priority = mkOption {
         default = 5;
         type = types.int;
@@ -146,11 +155,16 @@ in
 
               # Calculate memory to use for zram
               mem=$(${pkgs.gawk}/bin/awk '/MemTotal: / {
-                  print int($2*${toString cfg.memoryPercent}/100.0/${toString devicesCount}*1024)
+                  value=int($2*${toString cfg.memoryPercent}/100.0/${toString devicesCount}*1024);
+                    ${lib.optionalString (cfg.memoryMax != null) ''
+                      memory_max=int(${toString cfg.memoryMax}/${toString devicesCount});
+                      if (value > memory_max) { value = memory_max }
+                    ''}
+                  print value
               }' /proc/meminfo)
 
-              ${pkgs.utillinux}/sbin/zramctl --size $mem --algorithm ${cfg.algorithm} /dev/${dev}
-              ${pkgs.utillinux}/sbin/mkswap /dev/${dev}
+              ${pkgs.util-linux}/sbin/zramctl --size $mem --algorithm ${cfg.algorithm} /dev/${dev}
+              ${pkgs.util-linux}/sbin/mkswap /dev/${dev}
             '';
             restartIfChanged = false;
           };
diff --git a/nixos/modules/hardware/acpilight.nix b/nixos/modules/hardware/acpilight.nix
index 34e8a22209653..2de448a265c79 100644
--- a/nixos/modules/hardware/acpilight.nix
+++ b/nixos/modules/hardware/acpilight.nix
@@ -19,6 +19,7 @@ in
   };
 
   config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ acpilight ];
     services.udev.packages = with pkgs; [ acpilight ];
   };
 }
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index b07edb0f6acdf..8cf3e5633dc71 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -48,6 +48,7 @@ in {
         rtl8192su-firmware
         rt5677-firmware
         rtl8723bs-firmware
+        rtl8761b-firmware
         rtlwifi_new-firmware
         zd1211fw
         alsa-firmware
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
index e0ab37bca63a5..4aa1d6369d1bb 100644
--- a/nixos/modules/hardware/device-tree.nix
+++ b/nixos/modules/hardware/device-tree.nix
@@ -68,11 +68,11 @@ let
       patchShebangs scripts/*
       substituteInPlace scripts/Makefile.lib \
         --replace 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget))' 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget)) -@'
-      make ${pkgs.stdenv.hostPlatform.platform.kernelBaseConfig} ARCH="${pkgs.stdenv.hostPlatform.platform.kernelArch}"
-      make dtbs ARCH="${pkgs.stdenv.hostPlatform.platform.kernelArch}"
+      make ${pkgs.stdenv.hostPlatform.linux-kernel.baseConfig} ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
+      make dtbs ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
     '';
     installPhase = ''
-      make dtbs_install INSTALL_DTBS_PATH=$out/dtbs  ARCH="${pkgs.stdenv.hostPlatform.platform.kernelArch}"
+      make dtbs_install INSTALL_DTBS_PATH=$out/dtbs  ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
     '';
   };
 
@@ -115,7 +115,7 @@ in
   options = {
       hardware.deviceTree = {
         enable = mkOption {
-          default = pkgs.stdenv.hostPlatform.platform.kernelDTB or false;
+          default = pkgs.stdenv.hostPlatform.linux-kernel.DTB or false;
           type = types.bool;
           description = ''
             Build device tree files. These are used to describe the
diff --git a/nixos/modules/hardware/i2c.nix b/nixos/modules/hardware/i2c.nix
new file mode 100644
index 0000000000000..ff14b4b1c891d
--- /dev/null
+++ b/nixos/modules/hardware/i2c.nix
@@ -0,0 +1,43 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.i2c;
+in
+
+{
+  options.hardware.i2c = {
+    enable = mkEnableOption ''
+      i2c devices support. By default access is granted to users in the "i2c"
+      group (will be created if non-existent) and any user with a seat, meaning
+      logged on the computer locally.
+    '';
+
+    group = mkOption {
+      type = types.str;
+      default = "i2c";
+      description = ''
+        Grant access to i2c devices (/dev/i2c-*) to users in this group.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "i2c-dev" ];
+
+    users.groups = mkIf (cfg.group == "i2c") {
+      i2c = { };
+    };
+
+    services.udev.extraRules = ''
+      # allow group ${cfg.group} and users with a seat use of i2c devices
+      ACTION=="add", KERNEL=="i2c-[0-9]*", TAG+="uaccess", GROUP="${cfg.group}", MODE="660"
+    '';
+
+  };
+
+  meta.maintainers = [ maintainers.rnhmjoj ];
+
+}
diff --git a/nixos/modules/hardware/keyboard/zsa.nix b/nixos/modules/hardware/keyboard/zsa.nix
new file mode 100644
index 0000000000000..5cb09e5af499f
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/zsa.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkOption mkIf types;
+  cfg = config.hardware.keyboard.zsa;
+in
+{
+  # TODO: make group configurable like in https://github.com/NixOS/nixpkgs/blob/0b2b4b8c4e729535a61db56468809c5c2d3d175c/pkgs/tools/security/nitrokey-app/udev-rules.nix ?
+  options.hardware.keyboard.zsa = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
+        You need it when you want to flash a new configuration on the keyboard
+        or use their live training in the browser.
+        Access to the keyboard is granted to users in the "plugdev" group.
+        You may want to install the wally-cli package.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.zsa-udev-rules ];
+    users.groups.plugdev = {};
+  };
+}
diff --git a/nixos/modules/hardware/network/ath-user-regd.nix b/nixos/modules/hardware/network/ath-user-regd.nix
new file mode 100644
index 0000000000000..b5ade5ed50105
--- /dev/null
+++ b/nixos/modules/hardware/network/ath-user-regd.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  kernelVersion = config.boot.kernelPackages.kernel.version;
+  linuxKernelMinVersion = "5.8";
+  kernelPatch = pkgs.kernelPatches.ath_regd_optional // {
+    extraConfig = ''
+      ATH_USER_REGD y
+    '';
+  };
+in
+{
+  options.networking.wireless.athUserRegulatoryDomain = mkOption {
+    default = false;
+    type = types.bool;
+    description = ''
+      If enabled, sets the ATH_USER_REGD kernel config switch to true to
+      disable the enforcement of EEPROM regulatory restrictions for ath
+      drivers. Requires at least Linux ${linuxKernelMinVersion}.
+    '';
+  };
+
+  config = mkIf config.networking.wireless.athUserRegulatoryDomain {
+    assertions = singleton {
+      assertion = lessThan 0 (builtins.compareVersions kernelVersion linuxKernelMinVersion);
+      message = "ATH_USER_REGD patch for kernels older than ${linuxKernelMinVersion} not ported yet!";
+    };
+    boot.kernelPatches = [ kernelPatch ];
+  };
+}
diff --git a/nixos/modules/hardware/nitrokey.nix b/nixos/modules/hardware/nitrokey.nix
index 02e4c3f46f8d2..baa07203118c6 100644
--- a/nixos/modules/hardware/nitrokey.nix
+++ b/nixos/modules/hardware/nitrokey.nix
@@ -19,23 +19,9 @@ in
         nitrokey-app package, depending on your device and needs.
       '';
     };
-
-    group = mkOption {
-      type = types.str;
-      default = "nitrokey";
-      example = "wheel";
-      description = ''
-        Grant access to Nitrokey devices to users in this group.
-      '';
-    };
   };
 
   config = mkIf cfg.enable {
-    services.udev.packages = [
-      (pkgs.nitrokey-udev-rules.override (attrs:
-        { inherit (cfg) group; }
-      ))
-    ];
-    users.groups.${cfg.group} = {};
+    services.udev.packages = [ pkgs.nitrokey-udev-rules ];
   };
 }
diff --git a/nixos/modules/hardware/opentabletdriver.nix b/nixos/modules/hardware/opentabletdriver.nix
new file mode 100644
index 0000000000000..295e23e6164fa
--- /dev/null
+++ b/nixos/modules/hardware/opentabletdriver.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.opentabletdriver;
+in
+{
+  meta.maintainers = with lib.maintainers; [ thiagokokada ];
+
+  options = {
+    hardware.opentabletdriver = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable OpenTabletDriver udev rules, user service and blacklist kernel
+          modules known to conflict with OpenTabletDriver.
+        '';
+      };
+
+      blacklistedKernelModules = mkOption {
+        type = types.listOf types.str;
+        default = [ "hid-uclogic" "wacom" ];
+        description = ''
+          Blacklist of kernel modules known to conflict with OpenTabletDriver.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.opentabletdriver;
+        defaultText = "pkgs.opentabletdriver";
+        description = ''
+          OpenTabletDriver derivation to use.
+        '';
+      };
+
+      daemon = {
+        enable = mkOption {
+          default = true;
+          type = types.bool;
+          description = ''
+            Whether to start OpenTabletDriver daemon as a systemd user service.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    services.udev.packages = [ cfg.package ];
+
+    boot.blacklistedKernelModules = cfg.blacklistedKernelModules;
+
+    systemd.user.services.opentabletdriver = with pkgs; mkIf cfg.daemon.enable {
+      description = "Open source, cross-platform, user-mode tablet driver";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/otd-daemon -c ${cfg.package}/lib/OpenTabletDriver/Configurations";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/rtl-sdr.nix b/nixos/modules/hardware/rtl-sdr.nix
new file mode 100644
index 0000000000000..77c8cb59a3d58
--- /dev/null
+++ b/nixos/modules/hardware/rtl-sdr.nix
@@ -0,0 +1,20 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.rtl-sdr;
+
+in {
+  options.hardware.rtl-sdr = {
+    enable = lib.mkEnableOption ''
+      Enables rtl-sdr udev rules and ensures 'plugdev' group exists.
+      This is a prerequisite to using devices supported by rtl-sdr without
+      being root, since rtl-sdr USB descriptors will be owned by plugdev
+      through udev.
+    '';
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.udev.packages = [ pkgs.rtl-sdr ];
+    users.groups.plugdev = {};
+  };
+}
diff --git a/nixos/modules/hardware/sensor/hddtemp.nix b/nixos/modules/hardware/sensor/hddtemp.nix
new file mode 100644
index 0000000000000..df3f75e229a2f
--- /dev/null
+++ b/nixos/modules/hardware/sensor/hddtemp.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkIf mkOption types;
+
+  cfg = config.hardware.sensor.hddtemp;
+
+  wrapper = pkgs.writeShellScript "hddtemp-wrapper" ''
+    set -eEuo pipefail
+
+    file=/var/lib/hddtemp/hddtemp.db
+
+    drives=(${toString (map (e: ''$(realpath ${lib.escapeShellArg e}) '') cfg.drives)})
+
+    cp ${pkgs.hddtemp}/share/hddtemp/hddtemp.db $file
+    ${lib.concatMapStringsSep "\n" (e: "echo ${lib.escapeShellArg e} >> $file") cfg.dbEntries}
+
+    exec ${pkgs.hddtemp}/bin/hddtemp ${lib.escapeShellArgs cfg.extraArgs} \
+      --daemon \
+      --unit=${cfg.unit} \
+      --file=$file \
+      ''${drives[@]}
+  '';
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  ###### interface
+
+  options = {
+    hardware.sensor.hddtemp = {
+      enable = mkOption {
+        description = ''
+          Enable this option to support HDD/SSD temperature sensors.
+        '';
+        type = types.bool;
+        default = false;
+      };
+
+      drives = mkOption {
+        description = "List of drives to monitor. If you pass /dev/disk/by-path/* entries the symlinks will be resolved as hddtemp doesn't like names with colons.";
+        type = types.listOf types.str;
+      };
+
+      unit = mkOption {
+        description = "Celcius or Fahrenheit";
+        type = types.enum [ "C" "F" ];
+        default = "C";
+      };
+
+      dbEntries = mkOption {
+        description = "Additional DB entries";
+        type = types.listOf types.str;
+        default = [ ];
+      };
+
+      extraArgs = mkOption {
+        description = "Additional arguments passed to the daemon.";
+        type = types.listOf types.str;
+        default = [ ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.hddtemp = {
+      description = "HDD/SSD temperature";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = wrapper;
+        StateDirectory = "hddtemp";
+        PrivateTmp = true;
+        ProtectHome = "tmpfs";
+        ProtectSystem = "strict";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/video/bumblebee.nix b/nixos/modules/hardware/video/bumblebee.nix
index 2278c7b406117..b6af4f80445ab 100644
--- a/nixos/modules/hardware/video/bumblebee.nix
+++ b/nixos/modules/hardware/video/bumblebee.nix
@@ -40,7 +40,7 @@ in
         default = "wheel";
         example = "video";
         type = types.str;
-        description = ''Group for bumblebee socket'';
+        description = "Group for bumblebee socket";
       };
 
       connectDisplay = mkOption {
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index 2acb891f1a9ad..97accc7b99a03 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -5,36 +5,17 @@
 with lib;
 
 let
-
-  drivers = config.services.xserver.videoDrivers;
-
-  # FIXME: should introduce an option like
-  # ‘hardware.video.nvidia.package’ for overriding the default NVIDIA
-  # driver.
-  nvidiaForKernel = kernelPackages:
-    if elem "nvidia" drivers then
-        kernelPackages.nvidia_x11
-    else if elem "nvidiaBeta" drivers then
-        kernelPackages.nvidia_x11_beta
-    else if elem "nvidiaVulkanBeta" drivers then
-        kernelPackages.nvidia_x11_vulkan_beta
-    else if elem "nvidiaLegacy304" drivers then
-      kernelPackages.nvidia_x11_legacy304
-    else if elem "nvidiaLegacy340" drivers then
-      kernelPackages.nvidia_x11_legacy340
-    else if elem "nvidiaLegacy390" drivers then
-      kernelPackages.nvidia_x11_legacy390
-    else null;
-
-  nvidia_x11 = nvidiaForKernel config.boot.kernelPackages;
-  nvidia_libs32 =
-    if versionOlder nvidia_x11.version "391" then
-      ((nvidiaForKernel pkgs.pkgsi686Linux.linuxPackages).override { libsOnly = true; kernel = null; }).out
-    else
-      (nvidiaForKernel config.boot.kernelPackages).lib32;
+  nvidia_x11 = let
+    drivers = config.services.xserver.videoDrivers;
+    isDeprecated = str: (hasPrefix "nvidia" str) && (str != "nvidia");
+    hasDeprecated = drivers: any isDeprecated drivers;
+  in if (hasDeprecated drivers) then
+    throw ''
+      Selecting an nvidia driver has been modified for NixOS 19.03. The version is now set using `hardware.nvidia.package`.
+    ''
+  else if (elem "nvidia" drivers) then cfg.package else null;
 
   enabled = nvidia_x11 != null;
-
   cfg = config.hardware.nvidia;
 
   pCfg = cfg.prime;
@@ -63,6 +44,15 @@ in
       '';
     };
 
+    hardware.nvidia.powerManagement.finegrained = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Experimental power management of PRIME offload. For more information, see
+        the NVIDIA docs, chapter 22. PCI-Express runtime power management.
+      '';
+    };
+
     hardware.nvidia.modesetting.enable = mkOption {
       type = types.bool;
       default = false;
@@ -96,6 +86,16 @@ in
       '';
     };
 
+    hardware.nvidia.prime.amdgpuBusId = mkOption {
+      type = types.str;
+      default = "";
+      example = "PCI:4:0:0";
+      description = ''
+        Bus ID of the AMD APU. You can find it using lspci; for example if lspci
+	shows the AMD APU at "04:00.0", set this option to "PCI:4:0:0".
+      '';
+    };
+
     hardware.nvidia.prime.sync.enable = mkOption {
       type = types.bool;
       default = false;
@@ -151,9 +151,22 @@ in
         GPUs stay awake even during headless mode.
       '';
     };
+
+    hardware.nvidia.package = lib.mkOption {
+      type = lib.types.package;
+      default = config.boot.kernelPackages.nvidiaPackages.stable;
+      defaultText = "config.boot.kernelPackages.nvidiaPackages.stable";
+      description = ''
+        The NVIDIA X11 derivation to use.
+      '';
+      example = "config.boot.kernelPackages.nvidiaPackages.legacy340";
+    };
   };
 
-  config = mkIf enabled {
+  config = let
+      igpuDriver = if pCfg.intelBusId != "" then "modesetting" else "amdgpu";
+      igpuBusId = if pCfg.intelBusId != "" then pCfg.intelBusId else pCfg.amdgpuBusId;
+  in mkIf enabled {
     assertions = [
       {
         assertion = with config.services.xserver.displayManager; gdm.nvidiaWayland -> cfg.modesetting.enable;
@@ -161,7 +174,13 @@ in
       }
 
       {
-        assertion = primeEnabled -> pCfg.nvidiaBusId != "" && pCfg.intelBusId != "";
+        assertion = primeEnabled -> pCfg.intelBusId == "" || pCfg.amdgpuBusId == "";
+        message = ''
+          You cannot configure both an Intel iGPU and an AMD APU. Pick the one corresponding to your processor.
+        '';
+      }
+      {
+        assertion = primeEnabled -> pCfg.nvidiaBusId != "" && (pCfg.intelBusId != "" || pCfg.amdgpuBusId != "");
         message = ''
           When NVIDIA PRIME is enabled, the GPU bus IDs must configured.
         '';
@@ -174,6 +193,14 @@ in
         assertion = !(syncCfg.enable && offloadCfg.enable);
         message = "Only one NVIDIA PRIME solution may be used at a time.";
       }
+      {
+        assertion = !(syncCfg.enable && cfg.powerManagement.finegrained);
+        message = "Sync precludes powering down the NVIDIA GPU.";
+      }
+      {
+        assertion = cfg.powerManagement.enable -> offloadCfg.enable;
+        message = "Fine-grained power management requires offload to be enabled.";
+      }
     ];
 
     # If Optimus/PRIME is enabled, we:
@@ -183,18 +210,22 @@ in
     #   "nvidia" driver, in order to allow the X server to start without any outputs.
     # - Add a separate Device section for the Intel GPU, using the "modesetting"
     #   driver and with the configured BusID.
+    # - OR add a separate Device section for the AMD APU, using the "amdgpu"
+    #   driver and with the configures BusID.
     # - Reference that Device section from the ServerLayout section as an inactive
     #   device.
     # - Configure the display manager to run specific `xrandr` commands which will
-    #   configure/enable displays connected to the Intel GPU.
+    #   configure/enable displays connected to the Intel iGPU / AMD APU.
 
     services.xserver.useGlamor = mkDefault offloadCfg.enable;
 
-    services.xserver.drivers = optional primeEnabled {
-      name = "modesetting";
+    services.xserver.drivers = let
+    in optional primeEnabled {
+      name = igpuDriver;
       display = offloadCfg.enable;
+      modules = optional (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
       deviceSection = ''
-        BusID "${pCfg.intelBusId}"
+        BusID "${igpuBusId}"
         ${optionalString syncCfg.enable ''Option "AccelMethod" "none"''}
       '';
     } ++ singleton {
@@ -205,6 +236,7 @@ in
         ''
           BusID "${pCfg.nvidiaBusId}"
           ${optionalString syncCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
+          ${optionalString cfg.powerManagement.finegrained "Option \"NVreg_DynamicPowerManagement=0x02\""}
         '';
       screenSection =
         ''
@@ -214,14 +246,14 @@ in
     };
 
     services.xserver.serverLayoutSection = optionalString syncCfg.enable ''
-      Inactive "Device-modesetting[0]"
+      Inactive "Device-${igpuDriver}[0]"
     '' + optionalString offloadCfg.enable ''
       Option "AllowNVIDIAGPUScreens"
     '';
 
     services.xserver.displayManager.setupCommands = optionalString syncCfg.enable ''
       # Added by nvidia configuration module for Optimus/PRIME.
-      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource modesetting NVIDIA-0
+      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource ${igpuDriver} NVIDIA-0
       ${pkgs.xorg.xrandr}/bin/xrandr --auto
     '';
 
@@ -230,12 +262,12 @@ in
     };
 
     hardware.opengl.package = mkIf (!offloadCfg.enable) nvidia_x11.out;
-    hardware.opengl.package32 = mkIf (!offloadCfg.enable) nvidia_libs32;
+    hardware.opengl.package32 = mkIf (!offloadCfg.enable) nvidia_x11.lib32;
     hardware.opengl.extraPackages = optional offloadCfg.enable nvidia_x11.out;
-    hardware.opengl.extraPackages32 = optional offloadCfg.enable nvidia_libs32;
+    hardware.opengl.extraPackages32 = optional offloadCfg.enable nvidia_x11.lib32;
 
     environment.systemPackages = [ nvidia_x11.bin nvidia_x11.settings ]
-      ++ filter (p: p != null) [ nvidia_x11.persistenced ];
+      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ];
 
     systemd.packages = optional cfg.powerManagement.enable nvidia_x11.out;
 
@@ -292,16 +324,37 @@ in
     boot.kernelParams = optional (offloadCfg.enable || cfg.modesetting.enable) "nvidia-drm.modeset=1"
       ++ optional cfg.powerManagement.enable "nvidia.NVreg_PreserveVideoMemoryAllocations=1";
 
-    # Create /dev/nvidia-uvm when the nvidia-uvm module is loaded.
     services.udev.extraRules =
       ''
+        # Create /dev/nvidia-uvm when the nvidia-uvm module is loaded.
         KERNEL=="nvidia", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidiactl c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) 255'"
         KERNEL=="nvidia_modeset", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-modeset c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) 254'"
         KERNEL=="card*", SUBSYSTEM=="drm", DRIVERS=="nvidia", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia%n c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) %n'"
         KERNEL=="nvidia_uvm", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-uvm c $$(grep nvidia-uvm /proc/devices | cut -d \  -f 1) 0'"
         KERNEL=="nvidia_uvm", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-uvm-tools c $$(grep nvidia-uvm /proc/devices | cut -d \  -f 1) 0'"
+      '' + optionalString cfg.powerManagement.finegrained ''
+        # Remove NVIDIA USB xHCI Host Controller devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{remove}="1"
+
+        # Remove NVIDIA USB Type-C UCSI devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{remove}="1"
+
+        # Remove NVIDIA Audio devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{remove}="1"
+
+        # Enable runtime PM for NVIDIA VGA/3D controller devices on driver bind
+        ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto"
+        ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto"
+
+        # Disable runtime PM for NVIDIA VGA/3D controller devices on driver unbind
+        ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="on"
+        ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="on"
       '';
 
+    boot.extraModprobeConfig = mkIf cfg.powerManagement.finegrained ''
+      options nvidia "NVreg_DynamicPowerManagement=0x02"
+    '';
+
     boot.blacklistedKernelModules = [ "nouveau" "nvidiafb" ];
 
     services.acpid.enable = true;
diff --git a/nixos/modules/hardware/video/switcheroo-control.nix b/nixos/modules/hardware/video/switcheroo-control.nix
new file mode 100644
index 0000000000000..199adb2ad8f52
--- /dev/null
+++ b/nixos/modules/hardware/video/switcheroo-control.nix
@@ -0,0 +1,18 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  pkg = [ pkgs.switcheroo-control ];
+  cfg = config.services.switcherooControl;
+in {
+  options.services.switcherooControl = {
+    enable = mkEnableOption "switcheroo-control, a D-Bus service to check the availability of dual-GPU";
+  };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = pkg;
+    environment.systemPackages = pkg;
+    systemd.packages = pkg;
+    systemd.targets.multi-user.wants = [ "switcheroo-control.service" ];
+  };
+}
diff --git a/nixos/modules/i18n/input-method/default.nix b/nixos/modules/i18n/input-method/default.nix
index 0d6dd3399bfce..4649f9b862a5a 100644
--- a/nixos/modules/i18n/input-method/default.nix
+++ b/nixos/modules/i18n/input-method/default.nix
@@ -29,7 +29,7 @@ in
   options.i18n = {
     inputMethod = {
       enabled = mkOption {
-        type    = types.nullOr (types.enum [ "ibus" "fcitx" "nabi" "uim" "hime" ]);
+        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" ]);
         default = null;
         example = "fcitx";
         description = ''
@@ -42,6 +42,7 @@ in
           <itemizedlist>
           <listitem><para>ibus: The intelligent input bus, extra input engines can be added using <literal>i18n.inputMethod.ibus.engines</literal>.</para></listitem>
           <listitem><para>fcitx: A customizable lightweight input method, extra input engines can be added using <literal>i18n.inputMethod.fcitx.engines</literal>.</para></listitem>
+          <listitem><para>fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using <literal>i18n.inputMethod.fcitx5.addons</literal>.</para></listitem>
           <listitem><para>nabi: A Korean input method based on XIM. Nabi doesn't support Qt 5.</para></listitem>
           <listitem><para>uim: The universal input method, is a library with a XIM bridge. uim mainly support Chinese, Japanese and Korean.</para></listitem>
           <listitem><para>hime: An extremely easy-to-use input method framework.</para></listitem>
diff --git a/nixos/modules/i18n/input-method/default.xml b/nixos/modules/i18n/input-method/default.xml
index e15f666908190..73911059f8a61 100644
--- a/nixos/modules/i18n/input-method/default.xml
+++ b/nixos/modules/i18n/input-method/default.xml
@@ -252,8 +252,8 @@ i18n.inputMethod = {
   <para>
    Hime is an extremely easy-to-use input method framework. It is lightweight,
    stable, powerful and supports many commonly used input methods, including
-   Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Japanese Anthy, Korean Pinyin,
-   Latin Alphabet, Rancang hunting birds, cool music, etc...
+   Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Korean Pinyin, Latin Alphabet,
+   etc...
   </para>
 
   <para>
diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix
new file mode 100644
index 0000000000000..44962d202fe16
--- /dev/null
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -0,0 +1,33 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  im = config.i18n.inputMethod;
+  cfg = im.fcitx5;
+  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; };
+in
+  {
+    options = {
+      i18n.inputMethod.fcitx5 = {
+        addons = mkOption {
+          type = with types; listOf package;
+          default = [];
+          example = with pkgs; [ fcitx5-rime ];
+          description = ''
+            Enabled Fcitx5 addons.
+          '';
+        };
+      };
+    };
+
+    config = mkIf (im.enabled == "fcitx5") {
+      i18n.inputMethod.package = fcitx5Package;
+
+      environment.variables = {
+        GTK_IM_MODULE = "fcitx";
+        QT_IM_MODULE = "fcitx";
+        XMODIFIERS = "@im=fcitx";
+      };
+    };
+  }
diff --git a/nixos/modules/i18n/input-method/hime.nix b/nixos/modules/i18n/input-method/hime.nix
index a1b346a0f8403..8482130db3e34 100644
--- a/nixos/modules/i18n/input-method/hime.nix
+++ b/nixos/modules/i18n/input-method/hime.nix
@@ -1,23 +1,9 @@
-{ config, pkgs, ... }:
+{ config, pkgs, lib, ... }:
 
 with lib;
 {
-  options = {
-    i18n.inputMethod.hime = {
-      enableChewing = mkOption {
-        type    =  with types; nullOr bool;
-        default = null;
-        description = "enable chewing input method";
-      };
-      enableAnthy = mkOption {
-        type    =  with types; nullOr bool;
-        default = null;
-        description = "enable anthy input method";
-      };
-    };
-  };
-
   config = mkIf (config.i18n.inputMethod.enabled == "hime") {
+    i18n.inputMethod.package = pkgs.hime;
     environment.variables = {
       GTK_IM_MODULE = "hime";
       QT_IM_MODULE  = "hime";
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index cf24ecf586316..1aaa5a952bea2 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -48,7 +48,7 @@ in
       panel = mkOption {
         type = with types; nullOr path;
         default = null;
-        example = literalExample "''${pkgs.plasma5.plasma-desktop}/lib/libexec/kimpanel-ibus-panel";
+        example = literalExample "''${pkgs.plasma5Packages.plasma-desktop}/lib/libexec/kimpanel-ibus-panel";
         description = "Replace the IBus panel with another panel.";
       };
     };
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 8c98691116dc9..803bae4212ef7 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -9,7 +9,14 @@ with lib;
 
   isoImage.edition = "gnome";
 
-  services.xserver.desktopManager.gnome3.enable = true;
+  services.xserver.desktopManager.gnome3 = {
+    # Add firefox to favorite-apps
+    favoriteAppsOverride = ''
+      [org.gnome.shell]
+      favorite-apps=[ 'firefox.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop' ]
+    '';
+    enable = true;
+  };
 
   services.xserver.displayManager = {
     gdm = {
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 405fbfa10dbfd..1418420afcd98 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -143,6 +143,13 @@ let
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} loglevel=7
     INITRD /boot/${config.system.boot.loader.initrdFile}
+
+    # A variant to boot with a serial console enabled
+    LABEL boot-serial
+    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
+    LINUX /boot/${config.system.boot.loader.kernelFile}
+    APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} console=ttyS0,115200n8
+    INITRD /boot/${config.system.boot.loader.initrdFile}
   '';
 
   isolinuxMemtest86Entry = ''
@@ -418,7 +425,12 @@ in
     };
 
     isoImage.squashfsCompression = mkOption {
-      default = "xz -Xdict-size 100%";
+      default = with pkgs.stdenv.targetPlatform; "xz -Xdict-size 100% "
+                + lib.optionalString (isx86_32 || isx86_64) "-Xbcj x86"
+                # Untested but should also reduce size for these platforms
+                + lib.optionalString (isAarch32 || isAarch64) "-Xbcj arm"
+                + lib.optionalString (isPowerPC) "-Xbcj powerpc"
+                + lib.optionalString (isSparc) "-Xbcj sparc";
       description = ''
         Compression settings to use for the squashfs nix store.
       '';
diff --git a/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
index bef6cd2fb5a22..e4ec2d6240d02 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
@@ -17,8 +17,7 @@
   # The serial ports listed here are:
   # - ttyS0: for Tegra (Jetson TX1)
   # - ttyAMA0: for QEMU's -machine virt
-  # Also increase the amount of CMA to ensure the virtual console on the RPi3 works.
-  boot.kernelParams = ["cma=32M" "console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0"];
+  boot.kernelParams = ["console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0"];
 
   boot.initrd.availableKernelModules = [
     # Allows early (earlier) modesetting for the Raspberry Pi
@@ -30,13 +29,25 @@
   sdImage = {
     populateFirmwareCommands = let
       configTxt = pkgs.writeText "config.txt" ''
+        [pi3]
         kernel=u-boot-rpi3.bin
 
+        [pi4]
+        kernel=u-boot-rpi4.bin
+        enable_gic=1
+        armstub=armstub8-gic.bin
+
+        # Otherwise the resolution will be weird in most cases, compared to
+        # what the pi3 firmware does by default.
+        disable_overscan=1
+
+        [all]
         # Boot in 64-bit mode.
-        arm_control=0x200
+        arm_64bit=1
 
-        # U-Boot used to need this to work, regardless of whether UART is actually used or not.
-        # TODO: check when/if this can be removed.
+        # U-Boot needs this to work, regardless of whether UART is actually used or not.
+        # Look in arch/arm/mach-bcm283x/Kconfig in the U-Boot tree to see if this is still
+        # a requirement in the future.
         enable_uart=1
 
         # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
@@ -45,8 +56,17 @@
       '';
       in ''
         (cd ${pkgs.raspberrypifw}/share/raspberrypi/boot && cp bootcode.bin fixup*.dat start*.elf $NIX_BUILD_TOP/firmware/)
-        cp ${pkgs.ubootRaspberryPi3_64bit}/u-boot.bin firmware/u-boot-rpi3.bin
+
+        # Add the config
         cp ${configTxt} firmware/config.txt
+
+        # Add pi3 specific files
+        cp ${pkgs.ubootRaspberryPi3_64bit}/u-boot.bin firmware/u-boot-rpi3.bin
+
+        # Add pi4 specific files
+        cp ${pkgs.ubootRaspberryPi4_64bit}/u-boot.bin firmware/u-boot-rpi4.bin
+        cp ${pkgs.raspberrypi-armstubs}/armstub8-gic.bin firmware/armstub8-gic.bin
+        cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-4-b.dtb firmware/
       '';
     populateRootCommands = ''
       mkdir -p ./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 87545e8420308..5bdec7de86e8a 100644
--- a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix
@@ -3,36 +3,6 @@
 { config, lib, pkgs, ... }:
 
 {
-  imports = [
-    ../../profiles/base.nix
-    ../../profiles/installation-device.nix
-    ./sd-image.nix
-  ];
-
-  boot.loader.grub.enable = false;
-  boot.loader.raspberryPi.enable = true;
-  boot.loader.raspberryPi.version = 4;
+  imports = [ ./sd-image-aarch64.nix ];
   boot.kernelPackages = pkgs.linuxPackages_rpi4;
-
-  boot.consoleLogLevel = lib.mkDefault 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";
-    # As the boot process is done entirely in the firmware partition.
-    populateRootCommands = "";
-  };
-
-  fileSystems."/boot/firmware" = {
-    # This effectively "renames" the attrsOf 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 231c7bf0a6c25..b811ae07eb033 100644
--- a/nixos/modules/installer/cd-dvd/sd-image.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image.nix
@@ -147,10 +147,10 @@ in
     sdImage.storePaths = [ config.system.build.toplevel ];
 
     system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs,
-    mtools, libfaketime, utillinux, zstd }: stdenv.mkDerivation {
+    mtools, libfaketime, util-linux, zstd }: stdenv.mkDerivation {
       name = config.sdImage.imageName;
 
-      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux zstd ];
+      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime util-linux zstd ];
 
       inherit (config.sdImage) compressImage;
 
@@ -221,11 +221,12 @@ in
         set -euo pipefail
         set -x
         # Figure out device names for the boot device and root filesystem.
-        rootPart=$(${pkgs.utillinux}/bin/findmnt -n -o SOURCE /)
+        rootPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /)
         bootDevice=$(lsblk -npo PKNAME $rootPart)
+        partNum=$(lsblk -npo MAJ:MIN $rootPart | ${pkgs.gawk}/bin/awk -F: '{print $2}')
 
         # Resize the root partition and the filesystem to fit the disk
-        echo ",+," | sfdisk -N2 --no-reread $bootDevice
+        echo ",+," | sfdisk -N$partNum --no-reread $bootDevice
         ${pkgs.parted}/bin/partprobe
         ${pkgs.e2fsprogs}/bin/resize2fs $rootPart
 
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix b/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
index 6d4ba96dba0c1..123f487baf93c 100644
--- a/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
+++ b/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
@@ -26,7 +26,7 @@ let
   # A clue for the kernel loading
   kernelParams = pkgs.writeText "kernel-params.txt" ''
     Kernel Parameters:
-      init=/boot/init systemConfig=/boot/init ${toString config.boot.kernelParams}
+      init=/boot/init ${toString config.boot.kernelParams}
   '';
 
   # System wide nixpkgs config
@@ -104,7 +104,7 @@ in
     '';
 
   # Some more help text.
-  services.mingetty.helpLine =
+  services.getty.helpLine =
     ''
 
       Log in as "root" with an empty password.  ${
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc.nix b/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
index f2af7dcde3d54..a79209d7dfeff 100644
--- a/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
+++ b/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
@@ -23,13 +23,13 @@ let
     label nixos
       MENU LABEL ^NixOS using nfsroot
       KERNEL bzImage
-      append ip=dhcp nfsroot=/home/pcroot systemConfig=${config.system.build.toplevel} init=${config.system.build.toplevel}/init rw
+      append ip=dhcp nfsroot=/home/pcroot init=${config.system.build.toplevel}/init rw
 
     # I don't know how to make this boot with nfsroot (using the initrd)
     label nixos_initrd
       MENU LABEL NixOS booting the poor ^initrd.
       KERNEL bzImage
-      append initrd=initrd ip=dhcp nfsroot=/home/pcroot systemConfig=${config.system.build.toplevel} init=${config.system.build.toplevel}/init rw
+      append initrd=initrd ip=dhcp nfsroot=/home/pcroot init=${config.system.build.toplevel}/init rw
 
     label memtest
       MENU LABEL ^${pkgs.memtest86.name}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix b/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
index 8408f56f94f91..95579f3ca06d2 100644
--- a/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
+++ b/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
@@ -96,7 +96,7 @@ in
 
   boot.initrd.extraUtilsCommands =
     ''
-      copy_bin_and_libs ${pkgs.utillinux}/sbin/hwclock
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/hwclock
     '';
 
   boot.initrd.postDeviceCommands =
@@ -122,7 +122,7 @@ in
       device = "/dev/something";
     };
 
-  services.mingetty = {
+  services.getty = {
     # Some more help text.
     helpLine = ''
       Log in as "root" with an empty password.  ${
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
index 95eba86bcb654..fa074fdfcc6ea 100644
--- a/nixos/modules/installer/netboot/netboot.nix
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -88,7 +88,7 @@ with lib;
 
     system.build.netbootIpxeScript = pkgs.writeTextDir "netboot.ipxe" ''
       #!ipxe
-      kernel ${pkgs.stdenv.hostPlatform.platform.kernelTarget} init=${config.system.build.toplevel}/init initrd=initrd ${toString config.boot.kernelParams}
+      kernel ${pkgs.stdenv.hostPlatform.linux-kernel.target} init=${config.system.build.toplevel}/init initrd=initrd ${toString config.boot.kernelParams}
       initrd initrd
       boot
     '';
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index a15a2dbadb8c6..6b1f54beee2ea 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/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";
+  x86_64-linux = "/nix/store/iwfs2bfcy7lqwhri94p2i6jc87ih55zk-nix-2.3.10";
+  i686-linux = "/nix/store/a3ccfvy9i5n418d5v0bir330kbcz3vj8-nix-2.3.10";
+  aarch64-linux = "/nix/store/bh5g6cv7bv35iz853d3xv2sphn51ybmb-nix-2.3.10";
+  x86_64-darwin = "/nix/store/8c98r6zlwn2d40qm7jnnrr2rdlqviszr-nix-2.3.10";
 }
diff --git a/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix b/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
index 0c9f8522cc12b..e49ceba242459 100644
--- a/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
+++ b/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
@@ -15,4 +15,4 @@ with import ../../../../lib/testing-python.nix {
   pkgs = import ../../../../.. { inherit system config; };
 };
 
-(makeTest { inherit nodes; testScript = ""; }).driver
+(makeTest { inherit nodes; testScript = ""; }).driverInteractive
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
index c72ef6e9c28b3..450d776181489 100644
--- a/nixos/modules/installer/tools/nixos-enter.sh
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -69,6 +69,9 @@ mount --rbind /sys "$mountPoint/sys"
 
     # Run the activation script. Set $LOCALE_ARCHIVE to supress some Perl locale warnings.
     LOCALE_ARCHIVE="$system/sw/lib/locale/locale-archive" chroot "$mountPoint" "$system/activate" 1>&2 || true
+
+    # Create /tmp
+    chroot "$mountPoint" systemd-tmpfiles --create --remove --exclude-prefix=/dev 1>&2 || true
 )
 
 exec chroot "$mountPoint" "${command[@]}"
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 54b84521bdffd..7bc55e67134b6 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -183,6 +183,11 @@ sub pciCheck {
         push @imports, "(modulesPath + \"/hardware/network/broadcom-43xx.nix\")";
     }
 
+    # In case this is a virtio scsi device, we need to explicitly make this available.
+    if ($vendor eq "0x1af4" && $device eq "0x1004") {
+        push @initrdAvailableKernelModules, "virtio_scsi";
+    }
+
     # Can't rely on $module here, since the module may not be loaded
     # due to missing firmware.  Ideally we would check modules.pcimap
     # here.
@@ -580,6 +585,22 @@ EOF
     return $config;
 }
 
+sub generateXserverConfig {
+    my $xserverEnabled = "@xserverEnabled@";
+
+    my $config = "";
+    if ($xserverEnabled eq "1") {
+        $config = <<EOF;
+  # Enable the X11 windowing system.
+  services.xserver.enable = true;
+EOF
+    } else {
+        $config = <<EOF;
+  # Enable the X11 windowing system.
+  # services.xserver.enable = true;
+EOF
+    }
+}
 
 if ($showHardwareConfig) {
     print STDOUT $hwConfig;
@@ -625,6 +646,12 @@ EOF
 
         my $networkingDhcpConfig = generateNetworkingDhcpConfig();
 
+        my $xserverConfig = generateXserverConfig();
+
+        (my $desktopConfiguration = <<EOF)=~s/^/  /gm;
+@desktopConfiguration@
+EOF
+
         write_file($fn, <<EOF);
 @configuration@
 EOF
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index 90555f1458036..9d49d4055e431 100644
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -64,7 +64,7 @@ while [ "$#" -gt 0 ]; do
         --no-bootloader)
             noBootLoader=1
             ;;
-        --show-trace)
+        --show-trace|--impure|--keep-going)
             extraBuildFlags+=("$i")
             ;;
         --help)
diff --git a/nixos/modules/installer/tools/nixos-option/default.nix b/nixos/modules/installer/tools/nixos-option/default.nix
index 753fd92c7bbf6..72eec3a383634 100644
--- a/nixos/modules/installer/tools/nixos-option/default.nix
+++ b/nixos/modules/installer/tools/nixos-option/default.nix
@@ -1,11 +1,11 @@
-{lib, stdenv, boost, cmake, pkgconfig, nix, ... }:
+{lib, stdenv, boost, cmake, pkg-config, nix, ... }:
 stdenv.mkDerivation rec {
   name = "nixos-option";
   src = ./.;
-  nativeBuildInputs = [ cmake pkgconfig ];
+  nativeBuildInputs = [ cmake pkg-config ];
   buildInputs = [ boost nix ];
-  meta = {
-    license = stdenv.lib.licenses.lgpl2Plus;
-    maintainers = with lib.maintainers; [ chkno ];
+  meta = with lib; {
+    license = licenses.lgpl2Plus;
+    maintainers = with maintainers; [ chkno ];
   };
 }
diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
deleted file mode 100644
index e452e24d263d0..0000000000000
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ /dev/null
@@ -1,506 +0,0 @@
-#! @runtimeShell@
-
-if [ -x "@runtimeShell@" ]; then export SHELL="@runtimeShell@"; fi;
-
-set -e
-set -o pipefail
-
-export PATH=@path@:$PATH
-
-showSyntax() {
-    exec man nixos-rebuild
-    exit 1
-}
-
-
-# Parse the command line.
-origArgs=("$@")
-extraBuildFlags=()
-lockFlags=()
-flakeFlags=()
-action=
-buildNix=1
-fast=
-rollback=
-upgrade=
-upgrade_all=
-repair=
-profile=/nix/var/nix/profiles/system
-buildHost=
-targetHost=
-maybeSudo=()
-
-while [ "$#" -gt 0 ]; do
-    i="$1"; shift 1
-    case "$i" in
-      --help)
-        showSyntax
-        ;;
-      switch|boot|test|build|edit|dry-build|dry-run|dry-activate|build-vm|build-vm-with-bootloader)
-        if [ "$i" = dry-run ]; then i=dry-build; fi
-        action="$i"
-        ;;
-      --install-grub)
-        echo "$0: --install-grub deprecated, use --install-bootloader instead" >&2
-        export NIXOS_INSTALL_BOOTLOADER=1
-        ;;
-      --install-bootloader)
-        export NIXOS_INSTALL_BOOTLOADER=1
-        ;;
-      --no-build-nix)
-        buildNix=
-        ;;
-      --rollback)
-        rollback=1
-        ;;
-      --upgrade)
-        upgrade=1
-        ;;
-      --upgrade-all)
-        upgrade=1
-        upgrade_all=1
-        ;;
-      --repair)
-        repair=1
-        extraBuildFlags+=("$i")
-        ;;
-      --max-jobs|-j|--cores|-I|--builders)
-        j="$1"; shift 1
-        extraBuildFlags+=("$i" "$j")
-        ;;
-      --show-trace|--keep-failed|-K|--keep-going|-k|--verbose|-v|-vv|-vvv|-vvvv|-vvvvv|--fallback|--repair|--no-build-output|-Q|-j*|-L|--refresh|--no-net|--impure)
-        extraBuildFlags+=("$i")
-        ;;
-      --option)
-        j="$1"; shift 1
-        k="$1"; shift 1
-        extraBuildFlags+=("$i" "$j" "$k")
-        ;;
-      --fast)
-        buildNix=
-        fast=1
-        extraBuildFlags+=(--show-trace)
-        ;;
-      --profile-name|-p)
-        if [ -z "$1" ]; then
-            echo "$0: ‘--profile-name’ requires an argument"
-            exit 1
-        fi
-        if [ "$1" != system ]; then
-            profile="/nix/var/nix/profiles/system-profiles/$1"
-            mkdir -p -m 0755 "$(dirname "$profile")"
-        fi
-        shift 1
-        ;;
-      --build-host|h)
-        buildHost="$1"
-        shift 1
-        ;;
-      --target-host|t)
-        targetHost="$1"
-        shift 1
-        ;;
-      --use-remote-sudo)
-        maybeSudo=(sudo --)
-        ;;
-      --flake)
-        flake="$1"
-        flakeFlags=(--experimental-features 'nix-command flakes')
-        shift 1
-        ;;
-      --recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file)
-        lockFlags+=("$i")
-        ;;
-      --update-input)
-        j="$1"; shift 1
-        lockFlags+=("$i" "$j")
-        ;;
-      --override-input)
-        j="$1"; shift 1
-        k="$1"; shift 1
-        lockFlags+=("$i" "$j" "$k")
-        ;;
-      *)
-        echo "$0: unknown option \`$i'"
-        exit 1
-        ;;
-    esac
-done
-
-if [ -n "$SUDO_USER" ]; then
-    maybeSudo=(sudo --)
-fi
-
-if [ -z "$buildHost" -a -n "$targetHost" ]; then
-    buildHost="$targetHost"
-fi
-if [ "$targetHost" = localhost ]; then
-    targetHost=
-fi
-if [ "$buildHost" = localhost ]; then
-    buildHost=
-fi
-
-buildHostCmd() {
-    if [ -z "$buildHost" ]; then
-        "$@"
-    elif [ -n "$remoteNix" ]; then
-        ssh $SSHOPTS "$buildHost" env PATH="$remoteNix:$PATH" "${maybeSudo[@]}" "$@"
-    else
-        ssh $SSHOPTS "$buildHost" "${maybeSudo[@]}" "$@"
-    fi
-}
-
-targetHostCmd() {
-    if [ -z "$targetHost" ]; then
-        "${maybeSudo[@]}" "$@"
-    else
-        ssh $SSHOPTS "$targetHost" "${maybeSudo[@]}" "$@"
-    fi
-}
-
-copyToTarget() {
-    if ! [ "$targetHost" = "$buildHost" ]; then
-        if [ -z "$targetHost" ]; then
-            NIX_SSHOPTS=$SSHOPTS nix-copy-closure --from "$buildHost" "$1"
-        elif [ -z "$buildHost" ]; then
-            NIX_SSHOPTS=$SSHOPTS nix-copy-closure --to "$targetHost" "$1"
-        else
-            buildHostCmd nix-copy-closure --to "$targetHost" "$1"
-        fi
-    fi
-}
-
-nixBuild() {
-    if [ -z "$buildHost" ]; then
-        nix-build "$@"
-    else
-        local instArgs=()
-        local buildArgs=()
-
-        while [ "$#" -gt 0 ]; do
-            local i="$1"; shift 1
-            case "$i" in
-              -o)
-                local out="$1"; shift 1
-                buildArgs+=("--add-root" "$out" "--indirect")
-                ;;
-              -A)
-                local j="$1"; shift 1
-                instArgs+=("$i" "$j")
-                ;;
-              -I) # We don't want this in buildArgs
-                shift 1
-                ;;
-              --no-out-link) # We don't want this in buildArgs
-                ;;
-              "<"*) # nix paths
-                instArgs+=("$i")
-                ;;
-              *)
-                buildArgs+=("$i")
-                ;;
-            esac
-        done
-
-        local drv="$(nix-instantiate "${instArgs[@]}" "${extraBuildFlags[@]}")"
-        if [ -a "$drv" ]; then
-            NIX_SSHOPTS=$SSHOPTS nix-copy-closure --to "$buildHost" "$drv"
-            buildHostCmd nix-store -r "$drv" "${buildArgs[@]}"
-        else
-            echo "nix-instantiate failed"
-            exit 1
-        fi
-  fi
-}
-
-
-if [ -z "$action" ]; then showSyntax; fi
-
-# Only run shell scripts from the Nixpkgs tree if the action is
-# "switch", "boot", or "test". With other actions (such as "build"),
-# the user may reasonably expect that no code from the Nixpkgs tree is
-# executed, so it's safe to run nixos-rebuild against a potentially
-# untrusted tree.
-canRun=
-if [ "$action" = switch -o "$action" = boot -o "$action" = test ]; then
-    canRun=1
-fi
-
-
-# If ‘--upgrade’ or `--upgrade-all` is given,
-# run ‘nix-channel --update nixos’.
-if [[ -n $upgrade && -z $_NIXOS_REBUILD_REEXEC && -z $flake ]]; then
-    # If --upgrade-all is passed, or there are other channels that
-    # contain a file called ".update-on-nixos-rebuild", update them as
-    # well. Also upgrade the nixos channel.
-
-    for channelpath in /nix/var/nix/profiles/per-user/root/channels/*; do
-        channel_name=$(basename "$channelpath")
-
-        if [[ "$channel_name" == "nixos" ]]; then
-            nix-channel --update "$channel_name"
-        elif [ -e "$channelpath/.update-on-nixos-rebuild" ]; then
-            nix-channel --update "$channel_name"
-        elif [[ -n $upgrade_all ]] ; then
-            nix-channel --update "$channel_name"
-        fi
-    done
-fi
-
-# Make sure that we use the Nix package we depend on, not something
-# else from the PATH for nix-{env,instantiate,build}.  This is
-# important, because NixOS defaults the architecture of the rebuilt
-# system to the architecture of the nix-* binaries used.  So if on an
-# amd64 system the user has an i686 Nix package in her PATH, then we
-# would silently downgrade the whole system to be i686 NixOS on the
-# next reboot.
-if [ -z "$_NIXOS_REBUILD_REEXEC" ]; then
-    export PATH=@nix@/bin:$PATH
-fi
-
-# Use /etc/nixos/flake.nix if it exists. It can be a symlink to the
-# actual flake.
-if [[ -z $flake && -e /etc/nixos/flake.nix ]]; then
-    flake="$(dirname "$(readlink -f /etc/nixos/flake.nix)")"
-fi
-
-# Re-execute nixos-rebuild from the Nixpkgs tree.
-# FIXME: get nixos-rebuild from $flake.
-if [[ -z $_NIXOS_REBUILD_REEXEC && -n $canRun && -z $fast && -z $flake ]]; then
-    if p=$(nix-build --no-out-link --expr 'with import <nixpkgs/nixos> {}; config.system.build.nixos-rebuild' "${extraBuildFlags[@]}"); then
-        export _NIXOS_REBUILD_REEXEC=1
-        exec $p/bin/nixos-rebuild "${origArgs[@]}"
-        exit 1
-    fi
-fi
-
-# For convenience, use the hostname as the default configuration to
-# build from the flake.
-if [[ -n $flake ]]; then
-    if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
-       flake="${BASH_REMATCH[1]}"
-       flakeAttr="${BASH_REMATCH[2]}"
-    fi
-    if [[ -z $flakeAttr ]]; then
-        read -r hostname < /proc/sys/kernel/hostname
-        if [[ -z $hostname ]]; then
-            hostname=default
-        fi
-        flakeAttr="nixosConfigurations.\"$hostname\""
-    else
-        flakeAttr="nixosConfigurations.\"$flakeAttr\""
-    fi
-fi
-
-# Resolve the flake.
-if [[ -n $flake ]]; then
-    flake=$(nix "${flakeFlags[@]}" flake info --json "${extraBuildFlags[@]}" "${lockFlags[@]}" -- "$flake" | jq -r .url)
-fi
-
-# Find configuration.nix and open editor instead of building.
-if [ "$action" = edit ]; then
-    if [[ -z $flake ]]; then
-        NIXOS_CONFIG=${NIXOS_CONFIG:-$(nix-instantiate --find-file nixos-config)}
-        if [[ -d $NIXOS_CONFIG ]]; then
-            NIXOS_CONFIG=$NIXOS_CONFIG/default.nix
-        fi
-        exec ${EDITOR:-nano} "$NIXOS_CONFIG"
-    else
-        exec nix "${flakeFlags[@]}" edit "${lockFlags[@]}" -- "$flake#$flakeAttr"
-    fi
-    exit 1
-fi
-
-
-tmpDir=$(mktemp -t -d nixos-rebuild.XXXXXX)
-SSHOPTS="$NIX_SSHOPTS -o ControlMaster=auto -o ControlPath=$tmpDir/ssh-%n -o ControlPersist=60"
-
-cleanup() {
-    for ctrl in "$tmpDir"/ssh-*; do
-        ssh -o ControlPath="$ctrl" -O exit dummyhost 2>/dev/null || true
-    done
-    rm -rf "$tmpDir"
-}
-trap cleanup EXIT
-
-
-
-# If the Nix daemon is running, then use it.  This allows us to use
-# the latest Nix from Nixpkgs (below) for expression evaluation, while
-# still using the old Nix (via the daemon) for actual store access.
-# This matters if the new Nix in Nixpkgs has a schema change.  It
-# would upgrade the schema, which should only happen once we actually
-# switch to the new configuration.
-# If --repair is given, don't try to use the Nix daemon, because the
-# flag can only be used directly.
-if [ -z "$repair" ] && systemctl show nix-daemon.socket nix-daemon.service | grep -q ActiveState=active; then
-    export NIX_REMOTE=${NIX_REMOTE-daemon}
-fi
-
-
-# First build Nix, since NixOS may require a newer version than the
-# current one.
-if [ -n "$rollback" -o "$action" = dry-build ]; then
-    buildNix=
-fi
-
-nixSystem() {
-    machine="$(uname -m)"
-    if [[ "$machine" =~ i.86 ]]; then
-        machine=i686
-    fi
-    echo $machine-linux
-}
-
-prebuiltNix() {
-    machine="$1"
-    if [ "$machine" = x86_64 ]; then
-        echo @nix_x86_64_linux@
-    elif [[ "$machine" =~ i.86 ]]; then
-        echo @nix_i686_linux@
-    else
-        echo "$0: unsupported platform"
-        exit 1
-    fi
-}
-
-remotePATH=
-
-if [[ -n $buildNix && -z $flake ]]; then
-    echo "building Nix..." >&2
-    nixDrv=
-    if ! nixDrv="$(nix-instantiate '<nixpkgs/nixos>' --add-root $tmpDir/nix.drv --indirect -A config.nix.package.out "${extraBuildFlags[@]}")"; then
-        if ! nixDrv="$(nix-instantiate '<nixpkgs>' --add-root $tmpDir/nix.drv --indirect -A nix "${extraBuildFlags[@]}")"; then
-            if ! nixStorePath="$(nix-instantiate --eval '<nixpkgs/nixos/modules/installer/tools/nix-fallback-paths.nix>' -A $(nixSystem) | sed -e 's/^"//' -e 's/"$//')"; then
-                nixStorePath="$(prebuiltNix "$(uname -m)")"
-            fi
-            if ! nix-store -r $nixStorePath --add-root $tmpDir/nix --indirect \
-                --option extra-binary-caches https://cache.nixos.org/; then
-                echo "warning: don't know how to get latest Nix" >&2
-            fi
-            # Older version of nix-store -r don't support --add-root.
-            [ -e $tmpDir/nix ] || ln -sf $nixStorePath $tmpDir/nix
-            if [ -n "$buildHost" ]; then
-                remoteNixStorePath="$(prebuiltNix "$(buildHostCmd uname -m)")"
-                remoteNix="$remoteNixStorePath/bin"
-                if ! buildHostCmd nix-store -r $remoteNixStorePath \
-                  --option extra-binary-caches https://cache.nixos.org/ >/dev/null; then
-                    remoteNix=
-                    echo "warning: don't know how to get latest Nix" >&2
-                fi
-            fi
-        fi
-    fi
-    if [ -a "$nixDrv" ]; then
-        nix-store -r "$nixDrv"'!'"out" --add-root $tmpDir/nix --indirect >/dev/null
-        if [ -n "$buildHost" ]; then
-            nix-copy-closure --to "$buildHost" "$nixDrv"
-            # The nix build produces multiple outputs, we add them all to the remote path
-            for p in $(buildHostCmd nix-store -r "$(readlink "$nixDrv")" "${buildArgs[@]}"); do
-                remoteNix="$remoteNix${remoteNix:+:}$p/bin"
-            done
-        fi
-    fi
-    PATH="$tmpDir/nix/bin:$PATH"
-fi
-
-
-# Update the version suffix if we're building from Git (so that
-# nixos-version shows something useful).
-if [[ -n $canRun && -z $flake ]]; then
-    if nixpkgs=$(nix-instantiate --find-file nixpkgs "${extraBuildFlags[@]}"); then
-        suffix=$($SHELL $nixpkgs/nixos/modules/installer/tools/get-version-suffix "${extraBuildFlags[@]}" || true)
-        if [ -n "$suffix" ]; then
-            echo -n "$suffix" > "$nixpkgs/.version-suffix" || true
-        fi
-    fi
-fi
-
-
-if [ "$action" = dry-build ]; then
-    extraBuildFlags+=(--dry-run)
-fi
-
-
-# Either upgrade the configuration in the system profile (for "switch"
-# or "boot"), or just build it and create a symlink "result" in the
-# current directory (for "build" and "test").
-if [ -z "$rollback" ]; then
-    echo "building the system configuration..." >&2
-    if [ "$action" = switch -o "$action" = boot ]; then
-        if [[ -z $flake ]]; then
-            pathToConfig="$(nixBuild '<nixpkgs/nixos>' --no-out-link -A system "${extraBuildFlags[@]}")"
-        else
-            outLink=$tmpDir/result
-            nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.toplevel" \
-              "${extraBuildFlags[@]}" "${lockFlags[@]}" --out-link $outLink
-            pathToConfig="$(readlink -f $outLink)"
-        fi
-        copyToTarget "$pathToConfig"
-        targetHostCmd nix-env -p "$profile" --set "$pathToConfig"
-    elif [ "$action" = test -o "$action" = build -o "$action" = dry-build -o "$action" = dry-activate ]; then
-        if [[ -z $flake ]]; then
-            pathToConfig="$(nixBuild '<nixpkgs/nixos>' -A system -k "${extraBuildFlags[@]}")"
-        else
-            nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.toplevel" "${extraBuildFlags[@]}" "${lockFlags[@]}"
-            pathToConfig="$(readlink -f ./result)"
-        fi
-    elif [ "$action" = build-vm ]; then
-        if [[ -z $flake ]]; then
-            pathToConfig="$(nixBuild '<nixpkgs/nixos>' -A vm -k "${extraBuildFlags[@]}")"
-        else
-            nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.vm" \
-              "${extraBuildFlags[@]}" "${lockFlags[@]}"
-            pathToConfig="$(readlink -f ./result)"
-        fi
-    elif [ "$action" = build-vm-with-bootloader ]; then
-        if [[ -z $flake ]]; then
-            pathToConfig="$(nixBuild '<nixpkgs/nixos>' -A vmWithBootLoader -k "${extraBuildFlags[@]}")"
-        else
-            nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.vmWithBootLoader" \
-              "${extraBuildFlags[@]}" "${lockFlags[@]}"
-            pathToConfig="$(readlink -f ./result)"
-        fi
-    else
-        showSyntax
-    fi
-    # Copy build to target host if we haven't already done it
-    if ! [ "$action" = switch -o "$action" = boot ]; then
-        copyToTarget "$pathToConfig"
-    fi
-else # [ -n "$rollback" ]
-    if [ "$action" = switch -o "$action" = boot ]; then
-        targetHostCmd nix-env --rollback -p "$profile"
-        pathToConfig="$profile"
-    elif [ "$action" = test -o "$action" = build ]; then
-        systemNumber=$(
-            targetHostCmd nix-env -p "$profile" --list-generations |
-            sed -n '/current/ {g; p;}; s/ *\([0-9]*\).*/\1/; h'
-        )
-        pathToConfig="$profile"-${systemNumber}-link
-        if [ -z "$targetHost" ]; then
-            ln -sT "$pathToConfig" ./result
-        fi
-    else
-        showSyntax
-    fi
-fi
-
-
-# If we're not just building, then make the new configuration the boot
-# default and/or activate it now.
-if [ "$action" = switch -o "$action" = boot -o "$action" = test -o "$action" = dry-activate ]; then
-    if ! targetHostCmd $pathToConfig/bin/switch-to-configuration "$action"; then
-        echo "warning: error(s) occurred while switching to the new configuration" >&2
-        exit 1
-    fi
-fi
-
-
-if [ "$action" = build-vm ]; then
-    cat >&2 <<EOF
-
-Done.  The virtual machine can be started by running $(echo $pathToConfig/bin/run-*-vm)
-EOF
-fi
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 666a1eff0e62c..a9e5641b05a2e 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -28,24 +28,15 @@ let
     ];
   };
 
-  nixos-rebuild =
-    let fallback = import ./nix-fallback-paths.nix; in
-    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;
-      path = makeBinPath [ pkgs.jq ];
-    };
+  nixos-rebuild = pkgs.nixos-rebuild.override { nix = config.nix.package.out; };
 
   nixos-generate-config = makeProg {
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
     path = lib.optionals (lib.elem "btrfs" config.boot.supportedFilesystems) [ pkgs.btrfs-progs ];
     perl = "${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix}";
-    inherit (config.system.nixos-generate-config) configuration;
+    inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
+    xserverEnabled = config.services.xserver.enable;
   };
 
   nixos-option =
@@ -78,24 +69,42 @@ in
 
 {
 
-  options.system.nixos-generate-config.configuration = mkOption {
-    internal = true;
-    type = types.str;
-    description = ''
-      The NixOS module that <literal>nixos-generate-config</literal>
-      saves to <literal>/etc/nixos/configuration.nix</literal>.
-
-      This is an internal option. No backward compatibility is guaranteed.
-      Use at your own risk!
+  options.system.nixos-generate-config = {
+    configuration = mkOption {
+      internal = true;
+      type = types.str;
+      description = ''
+        The NixOS module that <literal>nixos-generate-config</literal>
+        saves to <literal>/etc/nixos/configuration.nix</literal>.
+
+        This is an internal option. No backward compatibility is guaranteed.
+        Use at your own risk!
+
+        Note that this string gets spliced into a Perl script. The perl
+        variable <literal>$bootLoaderConfig</literal> can be used to
+        splice in the boot loader configuration.
+      '';
+    };
 
-      Note that this string gets spliced into a Perl script. The perl
-      variable <literal>$bootLoaderConfig</literal> can be used to
-      splice in the boot loader configuration.
-    '';
+    desktopConfiguration = mkOption {
+      internal = true;
+      type = types.listOf types.lines;
+      default = [];
+      description = ''
+        Text to preseed the desktop configuration that <literal>nixos-generate-config</literal>
+        saves to <literal>/etc/nixos/configuration.nix</literal>.
+
+        This is an internal option. No backward compatibility is guaranteed.
+        Use at your own risk!
+
+        Note that this string gets spliced into a Perl script. The perl
+        variable <literal>$bootLoaderConfig</literal> can be used to
+        splice in the boot loader configuration.
+      '';
+    };
   };
 
   config = {
-
     system.nixos-generate-config.configuration = mkDefault ''
       # Edit this configuration file to define what should be installed on
       # your system.  Help is available in the configuration.nix(5) man page
@@ -113,6 +122,9 @@ in
         # networking.hostName = "nixos"; # Define your hostname.
         # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
 
+        # Set your time zone.
+        # time.timeZone = "Europe/Amsterdam";
+
       $networkingDhcpConfig
         # Configure network proxy if necessary
         # networking.proxy.default = "http://user:password\@proxy:port/";
@@ -125,13 +137,34 @@ in
         #   keyMap = "us";
         # };
 
-        # Set your time zone.
-        # time.timeZone = "Europe/Amsterdam";
+      $xserverConfig
+
+      $desktopConfiguration
+        # Configure keymap in X11
+        # services.xserver.layout = "us";
+        # services.xserver.xkbOptions = "eurosign:e";
+
+        # Enable CUPS to print documents.
+        # services.printing.enable = true;
+
+        # Enable sound.
+        # sound.enable = true;
+        # hardware.pulseaudio.enable = true;
+
+        # Enable touchpad support (enabled default in most desktopManager).
+        # services.xserver.libinput.enable = true;
+
+        # Define a user account. Don't forget to set a password with ‘passwd’.
+        # users.users.jane = {
+        #   isNormalUser = true;
+        #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
+        # };
 
         # List packages installed in system profile. To search, run:
         # \$ nix search wget
         # environment.systemPackages = with pkgs; [
         #   wget vim
+        #   firefox
         # ];
 
         # Some programs need SUID wrappers, can be configured further or are
@@ -140,7 +173,6 @@ in
         # programs.gnupg.agent = {
         #   enable = true;
         #   enableSSHSupport = true;
-        #   pinentryFlavor = "gnome3";
         # };
 
         # List services that you want to enable:
@@ -154,31 +186,6 @@ in
         # Or disable the firewall altogether.
         # networking.firewall.enable = false;
 
-        # Enable CUPS to print documents.
-        # services.printing.enable = true;
-
-        # Enable sound.
-        # sound.enable = true;
-        # hardware.pulseaudio.enable = true;
-
-        # Enable the X11 windowing system.
-        # services.xserver.enable = true;
-        # services.xserver.layout = "us";
-        # services.xserver.xkbOptions = "eurosign:e";
-
-        # Enable touchpad support.
-        # services.xserver.libinput.enable = true;
-
-        # Enable the KDE Desktop Environment.
-        # services.xserver.displayManager.sddm.enable = true;
-        # services.xserver.desktopManager.plasma5.enable = true;
-
-        # Define a user account. Don't forget to set a password with ‘passwd’.
-        # users.users.jane = {
-        #   isNormalUser = true;
-        #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
-        # };
-
         # This value determines the NixOS release from which the default
         # settings for stateful data, like file locations and database versions
         # on your system were taken. It‘s perfectly fine and recommended to leave
diff --git a/nixos/modules/misc/crashdump.nix b/nixos/modules/misc/crashdump.nix
index 3c47e79d05128..796078d7ef8c4 100644
--- a/nixos/modules/misc/crashdump.nix
+++ b/nixos/modules/misc/crashdump.nix
@@ -26,6 +26,7 @@ in
         };
         reservedMemory = mkOption {
           default = "128M";
+          type = types.str;
           description = ''
             The amount of memory reserved for the crashdump kernel.
             If you choose a too high value, dmesg will mention
@@ -52,7 +53,7 @@ in
         ${pkgs.kexectools}/sbin/kexec -p /run/current-system/kernel \
         --initrd=/run/current-system/initrd \
         --reset-vga --console-vga \
-        --command-line="systemConfig=$(readlink -f /run/current-system) init=$(readlink -f /run/current-system/init) irqpoll maxcpus=1 reset_devices ${kernelParams}"
+        --command-line="init=$(readlink -f /run/current-system/init) irqpoll maxcpus=1 reset_devices ${kernelParams}"
       '';
       kernelParams = [
        "crashkernel=${crashdump.reservedMemory}"
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 71a40b4f4d6e2..d81d6c6cb9b8c 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -40,9 +40,9 @@ let
       in scrubbedEval.options;
   };
 
-  helpScript = pkgs.writeScriptBin "nixos-help"
-    ''
-      #! ${pkgs.runtimeShell} -e
+
+  nixos-help = let
+    helpScript = pkgs.writeShellScriptBin "nixos-help" ''
       # Finds first executable browser in a colon-separated list.
       # (see how xdg-open defines BROWSER)
       browser="$(
@@ -59,14 +59,22 @@ let
       exec "$browser" ${manual.manualHTMLIndex}
     '';
 
-  desktopItem = pkgs.makeDesktopItem {
-    name = "nixos-manual";
-    desktopName = "NixOS Manual";
-    genericName = "View NixOS documentation in a web browser";
-    icon = "nix-snowflake";
-    exec = "${helpScript}/bin/nixos-help";
-    categories = "System";
-  };
+    desktopItem = pkgs.makeDesktopItem {
+      name = "nixos-manual";
+      desktopName = "NixOS Manual";
+      genericName = "View NixOS documentation in a web browser";
+      icon = "nix-snowflake";
+      exec = "nixos-help";
+      categories = "System";
+    };
+
+    in pkgs.symlinkJoin {
+      name = "nixos-help";
+      paths = [
+        helpScript
+        desktopItem
+      ];
+    };
 
 in
 
@@ -209,7 +217,7 @@ in
           manualCache = pkgs.runCommandLocal "man-cache" { }
           ''
             echo "MANDB_MAP ${manualPages}/share/man $out" > man.conf
-            ${pkgs.man-db}/bin/mandb -C man.conf -psc
+            ${pkgs.man-db}/bin/mandb -C man.conf -psc >/dev/null 2>&1
           '';
         in
         ''
@@ -250,10 +258,10 @@ in
 
       environment.systemPackages = []
         ++ optional cfg.man.enable manual.manpages
-        ++ optionals cfg.doc.enable ([ manual.manualHTML helpScript ]
-           ++ optionals config.services.xserver.enable [ desktopItem pkgs.nixos-icons ]);
+        ++ optionals cfg.doc.enable ([ manual.manualHTML nixos-help ]
+           ++ optionals config.services.xserver.enable [ pkgs.nixos-icons ]);
 
-      services.mingetty.helpLine = mkIf cfg.doc.enable (
+      services.getty.helpLine = mkIf cfg.doc.enable (
           "\nRun 'nixos-help' for the NixOS manual."
       );
     })
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 81d5d04fa5ea1..a0f5ce72f339b 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -71,7 +71,7 @@ in
       #utmp = 29; # unused
       # ddclient = 30; # converted to DynamicUser = true
       davfs2 = 31;
-      #disnix = 33; # unused
+      #disnix = 33; # module removed
       osgi = 34;
       tor = 35;
       cups = 36;
@@ -135,7 +135,7 @@ in
       #keys = 96; # unused
       #haproxy = 97; # dynamically allocated as of 2020-03-11
       mongodb = 98;
-      openldap = 99;
+      #openldap = 99; # dynamically allocated as of PR#94610
       #users = 100; # unused
       cgminer = 101;
       munin = 102;
@@ -143,7 +143,7 @@ in
       nix-ssh = 104;
       dictd = 105;
       couchdb = 106;
-      searx = 107;
+      #searx = 107; # dynamically allocated as of 2020-10-27
       kippo = 108;
       jenkins = 109;
       systemd-journal-gateway = 110;
@@ -290,14 +290,14 @@ in
       hound = 259;
       leaps = 260;
       ipfs  = 261;
-      stanchion = 262;
-      riak-cs = 263;
+      # stanchion = 262; # unused, removed 2020-10-14
+      # riak-cs = 263; # unused, removed 2020-10-14
       infinoted = 264;
       sickbeard = 265;
       headphones = 266;
       couchpotato = 267;
       gogs = 268;
-      pdns-recursor = 269;
+      #pdns-recursor = 269; # dynamically allocated as of 2020-20-18
       #kresd = 270; # switched to "knot-resolver" with dynamic ID
       rpc = 271;
       geoip = 272;
@@ -387,7 +387,7 @@ in
       utmp = 29;
       # ddclient = 30; # converted to DynamicUser = true
       davfs2 = 31;
-      disnix = 33;
+      #disnix = 33; # module removed
       osgi = 34;
       tor = 35;
       #cups = 36; # unused
@@ -451,13 +451,13 @@ in
       keys = 96;
       #haproxy = 97; # dynamically allocated as of 2020-03-11
       #mongodb = 98; # unused
-      openldap = 99;
+      #openldap = 99; # dynamically allocated as of PR#94610
       munin = 102;
       #logcheck = 103; # unused
       #nix-ssh = 104; # unused
       dictd = 105;
       couchdb = 106;
-      searx = 107;
+      #searx = 107; # dynamically allocated as of 2020-10-27
       kippo = 108;
       jenkins = 109;
       systemd-journal-gateway = 110;
@@ -468,7 +468,7 @@ in
       #minecraft = 114; # unused
       vault = 115;
       #ripped = 116; # unused
-      #murmur = 117; # unused
+      murmur = 117;
       foundationdb = 118;
       newrelic = 119;
       starbound = 120;
@@ -593,8 +593,8 @@ in
       hound = 259;
       leaps = 260;
       ipfs = 261;
-      stanchion = 262;
-      riak-cs = 263;
+      # stanchion = 262; # unused, removed 2020-10-14
+      # riak-cs = 263; # unused, removed 2020-10-14
       infinoted = 264;
       sickbeard = 265;
       headphones = 266;
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
index 92aa3be0a3667..1d2bc8c72813c 100644
--- a/nixos/modules/misc/locate.nix
+++ b/nixos/modules/misc/locate.nix
@@ -73,7 +73,72 @@ in {
 
     pruneFS = mkOption {
       type = listOf str;
-      default = ["afs" "anon_inodefs" "auto" "autofs" "bdev" "binfmt" "binfmt_misc" "cgroup" "cifs" "coda" "configfs" "cramfs" "cpuset" "debugfs" "devfs" "devpts" "devtmpfs" "ecryptfs" "eventpollfs" "exofs" "futexfs" "ftpfs" "fuse" "fusectl" "gfs" "gfs2" "hostfs" "hugetlbfs" "inotifyfs" "iso9660" "jffs2" "lustre" "misc" "mqueue" "ncpfs" "nnpfs" "ocfs" "ocfs2" "pipefs" "proc" "ramfs" "rpc_pipefs" "securityfs" "selinuxfs" "sfs" "shfs" "smbfs" "sockfs" "spufs" "nfs" "NFS" "nfs4" "nfsd" "sshfs" "subfs" "supermount" "sysfs" "tmpfs" "ubifs" "udf" "usbfs" "vboxsf" "vperfctrfs" ];
+      default = [
+        "afs"
+        "anon_inodefs"
+        "auto"
+        "autofs"
+        "bdev"
+        "binfmt"
+        "binfmt_misc"
+        "cgroup"
+        "cifs"
+        "coda"
+        "configfs"
+        "cramfs"
+        "cpuset"
+        "debugfs"
+        "devfs"
+        "devpts"
+        "devtmpfs"
+        "ecryptfs"
+        "eventpollfs"
+        "exofs"
+        "futexfs"
+        "ftpfs"
+        "fuse"
+        "fusectl"
+        "fuse.sshfs"
+        "gfs"
+        "gfs2"
+        "hostfs"
+        "hugetlbfs"
+        "inotifyfs"
+        "iso9660"
+        "jffs2"
+        "lustre"
+        "misc"
+        "mqueue"
+        "ncpfs"
+        "nnpfs"
+        "ocfs"
+        "ocfs2"
+        "pipefs"
+        "proc"
+        "ramfs"
+        "rpc_pipefs"
+        "securityfs"
+        "selinuxfs"
+        "sfs"
+        "shfs"
+        "smbfs"
+        "sockfs"
+        "spufs"
+        "nfs"
+        "NFS"
+        "nfs4"
+        "nfsd"
+        "sshfs"
+        "subfs"
+        "supermount"
+        "sysfs"
+        "tmpfs"
+        "ubifs"
+        "udf"
+        "usbfs"
+        "vboxsf"
+        "vperfctrfs"
+      ];
       description = ''
         Which filesystem types to exclude from indexing
       '';
@@ -150,7 +215,7 @@ in {
           ''
           else ''
             exec ${cfg.locate}/bin/updatedb \
-              ${optionalString (cfg.localuser != null && ! isMLocate) ''--localuser=${cfg.localuser}''} \
+              ${optionalString (cfg.localuser != null && ! isMLocate) "--localuser=${cfg.localuser}"} \
               --output=${toString cfg.output} ${concatStringsSep " " cfg.extraFlags}
           '';
         environment = optionalAttrs (!isMLocate) {
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
index 25ac94b8e0f65..8160bfef4a3cb 100644
--- a/nixos/modules/misc/nixpkgs.nix
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -73,7 +73,7 @@ in
           }
         '';
       type = pkgsType;
-      example = literalExample ''import <nixpkgs> {}'';
+      example = literalExample "import <nixpkgs> {}";
       description = ''
         If set, the pkgs argument to all NixOS modules is the value of
         this option, extended with <code>nixpkgs.overlays</code>, if
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 63df3c5779b14..644229627b2fc 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -46,11 +46,15 @@
   ./hardware/cpu/intel-microcode.nix
   ./hardware/digitalbitbox.nix
   ./hardware/device-tree.nix
+  ./hardware/i2c.nix
+  ./hardware/sensor/hddtemp.nix
   ./hardware/sensor/iio.nix
+  ./hardware/keyboard/zsa.nix
   ./hardware/ksm.nix
   ./hardware/ledger.nix
   ./hardware/logitech.nix
   ./hardware/mcelog.nix
+  ./hardware/network/ath-user-regd.nix
   ./hardware/network/b43.nix
   ./hardware/network/intel-2200bg.nix
   ./hardware/nitrokey.nix
@@ -59,11 +63,13 @@
   ./hardware/pcmcia.nix
   ./hardware/printers.nix
   ./hardware/raid/hpsa.nix
+  ./hardware/rtl-sdr.nix
   ./hardware/steam-hardware.nix
   ./hardware/system-76.nix
   ./hardware/tuxedo-keyboard.nix
   ./hardware/usb-wwan.nix
   ./hardware/onlykey.nix
+  ./hardware/opentabletdriver.nix
   ./hardware/wooting.nix
   ./hardware/uinput.nix
   ./hardware/video/amdgpu.nix
@@ -74,11 +80,14 @@
   ./hardware/video/displaylink.nix
   ./hardware/video/hidpi.nix
   ./hardware/video/nvidia.nix
+  ./hardware/video/switcheroo-control.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/fcitx5.nix
+  ./i18n/input-method/hime.nix
   ./i18n/input-method/ibus.nix
   ./i18n/input-method/nabi.nix
   ./i18n/input-method/uim.nix
@@ -97,6 +106,7 @@
   ./misc/version.nix
   ./misc/nixops-autoluks.nix
   ./programs/adb.nix
+  ./programs/appgate-sdp.nix
   ./programs/atop.nix
   ./programs/autojump.nix
   ./programs/bandwhich.nix
@@ -138,6 +148,7 @@
   ./programs/light.nix
   ./programs/mosh.nix
   ./programs/mininet.nix
+  ./programs/msmtp.nix
   ./programs/mtr.nix
   ./programs/nano.nix
   ./programs/neovim.nix
@@ -145,6 +156,7 @@
   ./programs/npm.nix
   ./programs/oblogout.nix
   ./programs/plotinus.nix
+  ./programs/proxychains.nix
   ./programs/qt5ct.nix
   ./programs/screen.nix
   ./programs/sedutil.nix
@@ -161,16 +173,17 @@
   ./programs/sway.nix
   ./programs/system-config-printer.nix
   ./programs/thefuck.nix
+  ./programs/tilp2.nix
   ./programs/tmux.nix
   ./programs/traceroute.nix
   ./programs/tsm-client.nix
   ./programs/udevil.nix
   ./programs/usbtop.nix
-  ./programs/venus.nix
   ./programs/vim.nix
   ./programs/wavemon.nix
   ./programs/waybar.nix
   ./programs/wireshark.nix
+  ./programs/wshowkeys.nix
   ./programs/x2goserver.nix
   ./programs/xfs_quota.nix
   ./programs/xonsh.nix
@@ -219,6 +232,7 @@
   ./services/audio/icecast.nix
   ./services/audio/liquidsoap.nix
   ./services/audio/mpd.nix
+  ./services/audio/mpdscribble.nix
   ./services/audio/mopidy.nix
   ./services/audio/roon-server.nix
   ./services/audio/slimserver.nix
@@ -293,8 +307,6 @@
   ./services/databases/postgresql.nix
   ./services/databases/redis.nix
   ./services/databases/riak.nix
-  ./services/databases/riak-cs.nix
-  ./services/databases/stanchion.nix
   ./services/databases/victoriametrics.nix
   ./services/databases/virtuoso.nix
   ./services/desktops/accountsservice.nix
@@ -309,7 +321,8 @@
   ./services/desktops/gsignond.nix
   ./services/desktops/gvfs.nix
   ./services/desktops/malcontent.nix
-  ./services/desktops/pipewire.nix
+  ./services/desktops/pipewire/pipewire.nix
+  ./services/desktops/pipewire/pipewire-media-session.nix
   ./services/desktops/gnome3/at-spi2-core.nix
   ./services/desktops/gnome3/chrome-gnome-shell.nix
   ./services/desktops/gnome3/evolution-data-server.nix
@@ -340,6 +353,7 @@
   ./services/editors/emacs.nix
   ./services/editors/infinoted.nix
   ./services/games/factorio.nix
+  ./services/games/freeciv.nix
   ./services/games/minecraft-server.nix
   ./services/games/minetest-server.nix
   ./services/games/openarena.nix
@@ -347,6 +361,7 @@
   ./services/games/terraria.nix
   ./services/hardware/acpid.nix
   ./services/hardware/actkbd.nix
+  ./services/hardware/auto-cpufreq.nix
   ./services/hardware/bluetooth.nix
   ./services/hardware/bolt.nix
   ./services/hardware/brltty.nix
@@ -361,6 +376,7 @@
   ./services/hardware/nvidia-optimus.nix
   ./services/hardware/pcscd.nix
   ./services/hardware/pommed.nix
+  ./services/hardware/power-profiles-daemon.nix
   ./services/hardware/ratbagd.nix
   ./services/hardware/sane.nix
   ./services/hardware/sane_extra_backends/brscan4.nix
@@ -391,16 +407,17 @@
   ./services/logging/logcheck.nix
   ./services/logging/logrotate.nix
   ./services/logging/logstash.nix
+  ./services/logging/promtail.nix
   ./services/logging/rsyslogd.nix
   ./services/logging/syslog-ng.nix
   ./services/logging/syslogd.nix
+  ./services/logging/vector.nix
   ./services/mail/clamsmtp.nix
   ./services/mail/davmail.nix
   ./services/mail/dkimproxy-out.nix
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
   ./services/mail/exim.nix
-  ./services/mail/freepops.nix
   ./services/mail/mail.nix
   ./services/mail/mailcatcher.nix
   ./services/mail/mailhog.nix
@@ -439,12 +456,12 @@
   ./services/misc/devmon.nix
   ./services/misc/dictd.nix
   ./services/misc/dwm-status.nix
-  ./services/misc/dysnomia.nix
-  ./services/misc/disnix.nix
   ./services/misc/docker-registry.nix
   ./services/misc/domoticz.nix
   ./services/misc/errbot.nix
   ./services/misc/etcd.nix
+  ./services/misc/etebase-server.nix
+  ./services/misc/etesync-dav.nix
   ./services/misc/ethminer.nix
   ./services/misc/exhibitor.nix
   ./services/misc/felix.nix
@@ -467,6 +484,7 @@
   ./services/misc/irkerd.nix
   ./services/misc/jackett.nix
   ./services/misc/jellyfin.nix
+  ./services/misc/klipper.nix
   ./services/misc/logkeys.nix
   ./services/misc/leaps.nix
   ./services/misc/lidarr.nix
@@ -478,12 +496,14 @@
   ./services/misc/mediatomb.nix
   ./services/misc/metabase.nix
   ./services/misc/mwlib.nix
+  ./services/misc/n8n.nix
   ./services/misc/nix-daemon.nix
   ./services/misc/nix-gc.nix
   ./services/misc/nix-optimise.nix
   ./services/misc/nix-ssh-serve.nix
   ./services/misc/novacomd.nix
   ./services/misc/nzbget.nix
+  ./services/misc/nzbhydra2.nix
   ./services/misc/octoprint.nix
   ./services/misc/osrm.nix
   ./services/misc/packagekit.nix
@@ -532,6 +552,7 @@
   ./services/monitoring/do-agent.nix
   ./services/monitoring/fusion-inventory.nix
   ./services/monitoring/grafana.nix
+  ./services/monitoring/grafana-image-renderer.nix
   ./services/monitoring/grafana-reporter.nix
   ./services/monitoring/graphite.nix
   ./services/monitoring/hdaps.nix
@@ -540,6 +561,7 @@
   ./services/monitoring/kapacitor.nix
   ./services/monitoring/loki.nix
   ./services/monitoring/longview.nix
+  ./services/monitoring/mackerel-agent.nix
   ./services/monitoring/monit.nix
   ./services/monitoring/munin.nix
   ./services/monitoring/nagios.nix
@@ -580,6 +602,7 @@
   ./services/network-filesystems/orangefs/client.nix
   ./services/network-filesystems/rsyncd.nix
   ./services/network-filesystems/samba.nix
+  ./services/network-filesystems/samba-wsdd.nix
   ./services/network-filesystems/tahoe.nix
   ./services/network-filesystems/diod.nix
   ./services/network-filesystems/u9fs.nix
@@ -593,6 +616,8 @@
   ./services/networking/atftpd.nix
   ./services/networking/avahi-daemon.nix
   ./services/networking/babeld.nix
+  ./services/networking/bee.nix
+  ./services/networking/bee-clef.nix
   ./services/networking/biboumi.nix
   ./services/networking/bind.nix
   ./services/networking/bitcoind.nix
@@ -618,6 +643,7 @@
   ./services/networking/dnsdist.nix
   ./services/networking/dnsmasq.nix
   ./services/networking/ncdns.nix
+  ./services/networking/nomad.nix
   ./services/networking/ejabberd.nix
   ./services/networking/epmd.nix
   ./services/networking/ergo.nix
@@ -628,7 +654,6 @@
   ./services/networking/fireqos.nix
   ./services/networking/firewall.nix
   ./services/networking/flannel.nix
-  ./services/networking/flashpolicyd.nix
   ./services/networking/freenet.nix
   ./services/networking/freeradius.nix
   ./services/networking/gale.nix
@@ -647,6 +672,8 @@
   ./services/networking/hylafax/default.nix
   ./services/networking/i2pd.nix
   ./services/networking/i2p.nix
+  ./services/networking/icecream/scheduler.nix
+  ./services/networking/icecream/daemon.nix
   ./services/networking/iodine.nix
   ./services/networking/iperf3.nix
   ./services/networking/ircd-hybrid/default.nix
@@ -678,6 +705,7 @@
   ./services/networking/murmur.nix
   ./services/networking/mxisd.nix
   ./services/networking/namecoind.nix
+  ./services/networking/nar-serve.nix
   ./services/networking/nat.nix
   ./services/networking/ndppd.nix
   ./services/networking/networkmanager.nix
@@ -706,6 +734,7 @@
   ./services/networking/owamp.nix
   ./services/networking/pdnsd.nix
   ./services/networking/pixiecore.nix
+  ./services/networking/pleroma.nix
   ./services/networking/polipo.nix
   ./services/networking/powerdns.nix
   ./services/networking/pdns-recursor.nix
@@ -732,6 +761,7 @@
   ./services/networking/skydns.nix
   ./services/networking/shadowsocks.nix
   ./services/networking/shairport-sync.nix
+  ./services/networking/shellhub-agent.nix
   ./services/networking/shorewall.nix
   ./services/networking/shorewall6.nix
   ./services/networking/shout.nix
@@ -837,7 +867,7 @@
   ./services/torrent/peerflix.nix
   ./services/torrent/rtorrent.nix
   ./services/torrent/transmission.nix
-  ./services/ttys/agetty.nix
+  ./services/ttys/getty.nix
   ./services/ttys/gpm.nix
   ./services/ttys/kmscon.nix
   ./services/wayland/cage.nix
@@ -846,28 +876,32 @@
   ./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/galene.nix
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
   ./services/web-apps/grocy.nix
+  ./services/web-apps/hedgedoc.nix
+  ./services/web-apps/hledger-web.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./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/keycloak.nix
   ./services/web-apps/limesurvey.nix
+  ./services/web-apps/mastodon.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
   ./services/web-apps/miniflux.nix
   ./services/web-apps/moodle.nix
   ./services/web-apps/nextcloud.nix
   ./services/web-apps/nexus.nix
+  ./services/web-apps/plantuml-server.nix
   ./services/web-apps/pgpkeyserver-lite.nix
   ./services/web-apps/matomo.nix
   ./services/web-apps/moinmoin.nix
@@ -880,6 +914,7 @@
   ./services/web-apps/selfoss.nix
   ./services/web-apps/shiori.nix
   ./services/web-apps/virtlyst.nix
+  ./services/web-apps/whitebophir.nix
   ./services/web-apps/wordpress.nix
   ./services/web-apps/youtrack.nix
   ./services/web-apps/zabbix.nix
@@ -935,6 +970,7 @@
   ./services/x11/urxvtd.nix
   ./services/x11/window-managers/awesome.nix
   ./services/x11/window-managers/default.nix
+  ./services/x11/window-managers/clfswm.nix
   ./services/x11/window-managers/fluxbox.nix
   ./services/x11/window-managers/icewm.nix
   ./services/x11/window-managers/bspwm.nix
diff --git a/nixos/modules/profiles/all-hardware.nix b/nixos/modules/profiles/all-hardware.nix
index 19f821ae17f39..d460c52dbefd4 100644
--- a/nixos/modules/profiles/all-hardware.nix
+++ b/nixos/modules/profiles/all-hardware.nix
@@ -3,8 +3,10 @@
 # enabled in the initrd.  Its primary use is in the NixOS installation
 # CDs.
 
-{ ... }:
-
+{ pkgs, lib,... }:
+let
+  platform = pkgs.stdenv.hostPlatform;
+in
 {
 
   # The initrd has to contain any module that might be necessary for
@@ -42,7 +44,10 @@
       "virtio_net" "virtio_pci" "virtio_blk" "virtio_scsi" "virtio_balloon" "virtio_console"
 
       # VMware support.
-      "mptspi" "vmw_balloon" "vmwgfx" "vmw_vmci" "vmw_vsock_vmci_transport" "vmxnet3" "vsock"
+      "mptspi" "vmxnet3" "vsock"
+    ] ++ lib.optional platform.isx86 "vmw_balloon"
+    ++ lib.optionals (!platform.isAarch64) [ # not sure where else they're missing
+      "vmw_vmci" "vmwgfx" "vmw_vsock_vmci_transport"
 
       # Hyper-V support.
       "hv_storvsc"
diff --git a/nixos/modules/profiles/hardened.nix b/nixos/modules/profiles/hardened.nix
index 42ed62b063044..680fa40b91195 100644
--- a/nixos/modules/profiles/hardened.nix
+++ b/nixos/modules/profiles/hardened.nix
@@ -1,5 +1,10 @@
 # A profile with most (vanilla) hardening options enabled by default,
-# potentially at the cost of features and performance.
+# potentially at the cost of stability, features and performance.
+#
+# This profile enables options that are known to affect system
+# stability. If you experience any stability issues when using the
+# profile, try disabling it. If you report an issue and use this
+# profile, always mention that you do.
 
 { config, lib, pkgs, ... }:
 
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index e68ea1b08776a..7dc493fb495da 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -45,10 +45,10 @@ with lib;
     };
 
     # Automatically log in at the virtual consoles.
-    services.mingetty.autologinUser = "nixos";
+    services.getty.autologinUser = "nixos";
 
     # Some more help text.
-    services.mingetty.helpLine = ''
+    services.getty.helpLine = ''
       The "nixos" and "root" accounts have empty passwords.
 
       An ssh daemon is running. You then must set a password
diff --git a/nixos/modules/profiles/qemu-guest.nix b/nixos/modules/profiles/qemu-guest.nix
index 0ea70107f7178..d4335edfcf2d3 100644
--- a/nixos/modules/profiles/qemu-guest.nix
+++ b/nixos/modules/profiles/qemu-guest.nix
@@ -1,7 +1,7 @@
 # Common configuration for virtual machines running under QEMU (using
 # virtio).
 
-{ lib, ... }:
+{ ... }:
 
 {
   boot.initrd.availableKernelModules = [ "virtio_net" "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_scsi" "9p" "9pnet_virtio" ];
@@ -14,6 +14,4 @@
       # to the *boot time* of the host).
       hwclock -s
     '';
-
-  security.rngd.enable = lib.mkDefault false;
 }
diff --git a/nixos/modules/programs/appgate-sdp.nix b/nixos/modules/programs/appgate-sdp.nix
new file mode 100644
index 0000000000000..1dec4ecf9eccb
--- /dev/null
+++ b/nixos/modules/programs/appgate-sdp.nix
@@ -0,0 +1,23 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    programs.appgate-sdp = {
+      enable = mkEnableOption
+        "AppGate SDP VPN client";
+    };
+  };
+
+  config = mkIf config.programs.appgate-sdp.enable {
+    boot.kernelModules = [ "tun" ];
+    environment.systemPackages = [ pkgs.appgate-sdp ];
+    services.dbus.packages = [ pkgs.appgate-sdp ];
+    systemd = {
+      packages = [ pkgs.appgate-sdp ];
+      # https://github.com/NixOS/nixpkgs/issues/81138
+      services.appgatedriver.wantedBy =  [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/programs/bandwhich.nix b/nixos/modules/programs/bandwhich.nix
index 5413044f46141..1cffb5fa2765c 100644
--- a/nixos/modules/programs/bandwhich.nix
+++ b/nixos/modules/programs/bandwhich.nix
@@ -4,7 +4,7 @@ with lib;
 
 let cfg = config.programs.bandwhich;
 in {
-  meta.maintainers = with maintainers; [ filalex77 ];
+  meta.maintainers = with maintainers; [ Br1ght0ne ];
 
   options = {
     programs.bandwhich = {
diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix
index 26db167507271..1f223e2475ce4 100644
--- a/nixos/modules/programs/captive-browser.nix
+++ b/nixos/modules/programs/captive-browser.nix
@@ -1,7 +1,6 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
   cfg = config.programs.captive-browser;
 in
@@ -27,15 +26,17 @@ in
       # the options below are the same as in "captive-browser.toml"
       browser = mkOption {
         type = types.str;
-        default = concatStringsSep " " [ ''${pkgs.chromium}/bin/chromium''
-                                         ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
-                                         ''--proxy-server="socks5://$PROXY"''
-                                         ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
-                                         ''--no-first-run''
-                                         ''--new-window''
-                                         ''--incognito''
-                                         ''http://cache.nixos.org/''
-                                       ];
+        default = concatStringsSep " " [
+          ''${pkgs.chromium}/bin/chromium''
+          ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
+          ''--proxy-server="socks5://$PROXY"''
+          ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
+          ''--no-first-run''
+          ''--new-window''
+          ''--incognito''
+          ''-no-default-browser-check''
+          ''http://cache.nixos.org/''
+        ];
         description = ''
           The shell (/bin/sh) command executed once the proxy starts.
           When browser exits, the proxy exits. An extra env var PROXY is available.
@@ -62,7 +63,7 @@ in
       socks5-addr = mkOption {
         type = types.str;
         default = "localhost:1666";
-        description = ''the listen address for the SOCKS5 proxy server'';
+        description = "the listen address for the SOCKS5 proxy server";
       };
 
       bindInterface = mkOption {
@@ -81,42 +82,45 @@ in
 
   config = mkIf cfg.enable {
 
-    programs.captive-browser.dhcp-dns = mkOptionDefault (
-      if config.networking.networkmanager.enable then
-        "${pkgs.networkmanager}/bin/nmcli dev show ${escapeShellArg cfg.interface} | ${pkgs.gnugrep}/bin/fgrep IP4.DNS"
-      else if config.networking.dhcpcd.enable then
-        "${pkgs.dhcpcd}/bin/dhcpcd -U ${escapeShellArg cfg.interface} | ${pkgs.gnugrep}/bin/fgrep domain_name_servers"
-      else if config.networking.useNetworkd then
-        "${cfg.package}/bin/systemd-networkd-dns ${escapeShellArg cfg.interface}"
-      else
-        "${config.security.wrapperDir}/udhcpc --quit --now -f -i ${escapeShellArg cfg.interface} -O dns --script ${
-            pkgs.writeScript "udhcp-script" ''
-              #!/bin/sh
-              if [ "$1" = bound ]; then
-                echo "$dns"
-              fi
-            ''}"
-    );
+    programs.captive-browser.dhcp-dns =
+      let
+        iface = prefix:
+          optionalString cfg.bindInterface (concatStringsSep " " (map escapeShellArg [ prefix cfg.interface ]));
+      in
+      mkOptionDefault (
+        if config.networking.networkmanager.enable then
+          "${pkgs.networkmanager}/bin/nmcli dev show ${iface ""} | ${pkgs.gnugrep}/bin/fgrep IP4.DNS"
+        else if config.networking.dhcpcd.enable then
+          "${pkgs.dhcpcd}/bin/dhcpcd ${iface "-U"} | ${pkgs.gnugrep}/bin/fgrep domain_name_servers"
+        else if config.networking.useNetworkd then
+          "${cfg.package}/bin/systemd-networkd-dns ${iface ""}"
+        else
+          "${config.security.wrapperDir}/udhcpc --quit --now -f ${iface "-i"} -O dns --script ${
+          pkgs.writeShellScript "udhcp-script" ''
+            if [ "$1" = bound ]; then
+              echo "$dns"
+            fi
+          ''}"
+      );
 
     security.wrappers.udhcpc = {
-      capabilities  = "cap_net_raw+p";
-      source        = "${pkgs.busybox}/bin/udhcpc";
+      capabilities = "cap_net_raw+p";
+      source = "${pkgs.busybox}/bin/udhcpc";
     };
 
     security.wrappers.captive-browser = {
-      capabilities  = "cap_net_raw+p";
-      source        = pkgs.writeScript "captive-browser" ''
-                        #!${pkgs.bash}/bin/bash
-                        export XDG_CONFIG_HOME=${pkgs.writeTextDir "captive-browser.toml" ''
-                                                  browser = """${cfg.browser}"""
-                                                  dhcp-dns = """${cfg.dhcp-dns}"""
-                                                  socks5-addr = """${cfg.socks5-addr}"""
-                                                  ${optionalString cfg.bindInterface ''
-                                                    bind-device = """${cfg.interface}"""
-                                                  ''}
-                                                ''}
-                        exec ${cfg.package}/bin/captive-browser
-                      '';
+      capabilities = "cap_net_raw+p";
+      source = pkgs.writeShellScript "captive-browser" ''
+        export XDG_CONFIG_HOME=${pkgs.writeTextDir "captive-browser.toml" ''
+                                  browser = """${cfg.browser}"""
+                                  dhcp-dns = """${cfg.dhcp-dns}"""
+                                  socks5-addr = """${cfg.socks5-addr}"""
+                                  ${optionalString cfg.bindInterface ''
+                                    bind-device = """${cfg.interface}"""
+                                  ''}
+                                ''}
+        exec ${cfg.package}/bin/captive-browser
+      '';
     };
   };
 }
diff --git a/nixos/modules/programs/cdemu.nix b/nixos/modules/programs/cdemu.nix
index a59cd93cadfc0..142e293424057 100644
--- a/nixos/modules/programs/cdemu.nix
+++ b/nixos/modules/programs/cdemu.nix
@@ -16,18 +16,21 @@ in {
         '';
       };
       group = mkOption {
+        type = types.str;
         default = "cdrom";
         description = ''
           Group that users must be in to use <command>cdemu</command>.
         '';
       };
       gui = mkOption {
+        type = types.bool;
         default = true;
         description = ''
           Whether to install the <command>cdemu</command> GUI (gCDEmu).
         '';
       };
       image-analyzer = mkOption {
+        type = types.bool;
         default = true;
         description = ''
           Whether to install the image analyzer.
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
index 3f0429136190f..b727f850a949b 100644
--- a/nixos/modules/programs/chromium.nix
+++ b/nixos/modules/programs/chromium.nix
@@ -29,7 +29,7 @@ in
           page. To install a chromium extension not included in the chrome web
           store, append to the extension id a semicolon ";" followed by a URL
           pointing to an Update Manifest XML file. See
-          <link xlink:href="https://www.chromium.org/administrators/policy-list-3#ExtensionInstallForcelist">ExtensionInstallForcelist</link>
+          <link xlink:href="https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExtensionInstallForcelist">ExtensionInstallForcelist</link>
           for additional details.
         '';
         default = [];
diff --git a/nixos/modules/programs/command-not-found/command-not-found.nix b/nixos/modules/programs/command-not-found/command-not-found.nix
index 656c255fcb185..d8394bf73a2e8 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.nix
+++ b/nixos/modules/programs/command-not-found/command-not-found.nix
@@ -80,6 +80,8 @@ in
             # Retry the command if we just installed it.
             if [ $? = 126 ]; then
               "$@"
+            else
+              return 127
             fi
           else
             # Indicate than there was an error so ZSH falls back to its default handler
diff --git a/nixos/modules/programs/command-not-found/command-not-found.pl b/nixos/modules/programs/command-not-found/command-not-found.pl
index ab7aa204653cd..7515dd975c316 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.pl
+++ b/nixos/modules/programs/command-not-found/command-not-found.pl
@@ -27,8 +27,8 @@ if (!defined $res || scalar @$res == 0) {
     my $package = @$res[0]->{package};
     if ($ENV{"NIX_AUTO_INSTALL"} // "") {
         print STDERR <<EOF;
-The program ‘$program’ is currently not installed. It is provided by
-the package ‘$package’, which I will now install for you.
+The program '$program' is currently not installed. It is provided by
+the package '$package', which I will now install for you.
 EOF
         ;
         exit 126 if system("nix-env", "-iA", "nixos.$package") == 0;
@@ -36,16 +36,17 @@ EOF
         exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV));
     } else {
         print STDERR <<EOF;
-The program ‘$program’ is currently not installed. You can install it by typing:
-  nix-env -iA nixos.$package
+The program '$program' is not in your PATH. You can make it available in an
+ephemeral shell by typing:
+  nix-shell -p $package
 EOF
     }
 } else {
     print STDERR <<EOF;
-The program ‘$program’ is currently not installed. It is provided by
-several packages. You can install it by typing one of the following:
+The program '$program' is not in your PATH. It is provided by several packages.
+You can make it available in an ephemeral shell by typing one of the following:
 EOF
-    print STDERR "  nix-env -iA nixos.$_->{package}\n" foreach @$res;
+    print STDERR "  nix-shell -p $_->{package}\n" foreach @$res;
 }
 
 exit 127;
diff --git a/nixos/modules/programs/firejail.nix b/nixos/modules/programs/firejail.nix
index 484f9eb444060..ad4ef1a39459d 100644
--- a/nixos/modules/programs/firejail.nix
+++ b/nixos/modules/programs/firejail.nix
@@ -11,10 +11,20 @@ let
     }
     ''
       mkdir -p $out/bin
-      ${lib.concatStringsSep "\n" (lib.mapAttrsToList (command: binary: ''
+      ${lib.concatStringsSep "\n" (lib.mapAttrsToList (command: value:
+      let
+        opts = if builtins.isAttrs value
+        then value
+        else { executable = value; profile = null; extraArgs = []; };
+        args = lib.escapeShellArgs (
+          (optional (opts.profile != null) "--profile=${toString opts.profile}")
+          ++ opts.extraArgs
+          );
+      in
+      ''
         cat <<_EOF >$out/bin/${command}
         #! ${pkgs.runtimeShell} -e
-        exec /run/wrappers/bin/firejail ${binary} "\$@"
+        exec /run/wrappers/bin/firejail ${args} -- ${toString opts.executable} "\$@"
         _EOF
         chmod 0755 $out/bin/${command}
       '') cfg.wrappedBinaries)}
@@ -25,12 +35,38 @@ in {
     enable = mkEnableOption "firejail";
 
     wrappedBinaries = mkOption {
-      type = types.attrsOf types.path;
+      type = types.attrsOf (types.either types.path (types.submodule {
+        options = {
+          executable = mkOption {
+            type = types.path;
+            description = "Executable to run sandboxed";
+            example = literalExample "''${lib.getBin pkgs.firefox}/bin/firefox";
+          };
+          profile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Profile to use";
+            example = literalExample "''${pkgs.firejail}/etc/firejail/firefox.profile";
+          };
+          extraArgs = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            description = "Extra arguments to pass to firejail";
+            example = [ "--private=~/.firejail_home" ];
+          };
+        };
+      }));
       default = {};
       example = literalExample ''
         {
-          firefox = "''${lib.getBin pkgs.firefox}/bin/firefox";
-          mpv = "''${lib.getBin pkgs.mpv}/bin/mpv";
+          firefox = {
+            executable = "''${lib.getBin pkgs.firefox}/bin/firefox";
+            profile = "''${pkgs.firejail}/etc/firejail/firefox.profile";
+          };
+          mpv = {
+            executable = "''${lib.getBin pkgs.mpv}/bin/mpv";
+            profile = "''${pkgs.firejail}/etc/firejail/mpv.profile";
+          };
         }
       '';
       description = ''
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index 39b92edf2ac2e..392f06eb93326 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -13,6 +13,27 @@ let
       (filterAttrs (k: v: v != null) cfg.shellAliases)
   );
 
+  envShellInit = pkgs.writeText "shellInit" cfge.shellInit;
+
+  envLoginShellInit = pkgs.writeText "loginShellInit" cfge.loginShellInit;
+
+  envInteractiveShellInit = pkgs.writeText "interactiveShellInit" cfge.interactiveShellInit;
+
+  sourceEnv = file:
+  if cfg.useBabelfish then
+    "source /etc/fish/${file}.fish"
+  else
+    ''
+      set fish_function_path ${pkgs.fishPlugins.foreign-env}/share/fish/vendor_functions.d $fish_function_path
+      fenv source /etc/fish/foreign-env/${file} > /dev/null
+      set -e fish_function_path[1]
+    '';
+
+  babelfishTranslate = path: name:
+    pkgs.runCommand "${name}.fish" {
+      nativeBuildInputs = [ pkgs.babelfish ];
+    } "${pkgs.babelfish}/bin/babelfish < ${path} > $out;";
+
 in
 
 {
@@ -29,6 +50,15 @@ in
         type = types.bool;
       };
 
+      useBabelfish = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, the configured environment will be translated to native fish using <link xlink:href="https://github.com/bouk/babelfish">babelfish</link>.
+          Otherwise, <link xlink:href="https://github.com/oh-my-fish/plugin-foreign-env">foreign-env</link> will be used.
+        '';
+      };
+
       vendor.config.enable = mkOption {
         type = types.bool;
         default = true;
@@ -103,74 +133,154 @@ 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;
-
-    environment.etc."fish/nixos-env-preinit.fish".text = ''
-      # This happens before $__fish_datadir/config.fish sets fish_function_path, so it is currently
-      # unset. We set it and then completely erase it, leaving its configuration to $__fish_datadir/config.fish
-      set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $__fish_datadir/functions
-
-      # source the NixOS environment config
-      if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]
-          fenv source ${config.system.build.setEnvironment}
-      end
-
-      # clear fish_function_path so that it will be correctly set when we return to $__fish_datadir/config.fish
-      set -e fish_function_path
-    '';
-
-    environment.etc."fish/config.fish".text = ''
-      # /etc/fish/config.fish: DO NOT EDIT -- this file has been generated automatically.
-
-      # if we haven't sourced the general config, do it
-      if not set -q __fish_nixos_general_config_sourced
-        set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $fish_function_path
-        fenv source /etc/fish/foreign-env/shellInit > /dev/null
-        set -e fish_function_path[1]
-
-        ${cfg.shellInit}
-
-        # and leave a note so we don't source this config section again from
-        # this very shell (children will source the general config anew)
-        set -g __fish_nixos_general_config_sourced 1
-      end
-
-      # if we haven't sourced the login config, do it
-      status --is-login; and not set -q __fish_nixos_login_config_sourced
-      and begin
-        set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $fish_function_path
-        fenv source /etc/fish/foreign-env/loginShellInit > /dev/null
-        set -e fish_function_path[1]
-
-        ${cfg.loginShellInit}
-
-        # and leave a note so we don't source this config section again from
-        # this very shell (children will source the general config anew)
-        set -g __fish_nixos_login_config_sourced 1
-      end
-
-      # if we haven't sourced the interactive config, do it
-      status --is-interactive; and not set -q __fish_nixos_interactive_config_sourced
-      and begin
-        ${fishAliases}
-
-        set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $fish_function_path
-        fenv source /etc/fish/foreign-env/interactiveShellInit > /dev/null
-        set -e fish_function_path[1]
-
-        ${cfg.promptInit}
-        ${cfg.interactiveShellInit}
-
-        # and leave a note so we don't source this config section again from
-        # this very shell (children will source the general config anew,
-        # allowing configuration changes in, e.g, aliases, to propagate)
-        set -g __fish_nixos_interactive_config_sourced 1
-      end
-    '';
+    documentation.man.generateCaches = lib.mkDefault true;
+
+    environment = mkMerge [
+      (mkIf cfg.useBabelfish
+      {
+        etc."fish/setEnvironment.fish".source = babelfishTranslate config.system.build.setEnvironment "setEnvironment";
+        etc."fish/shellInit.fish".source = babelfishTranslate envShellInit "shellInit";
+        etc."fish/loginShellInit.fish".source = babelfishTranslate envLoginShellInit "loginShellInit";
+        etc."fish/interactiveShellInit.fish".source = babelfishTranslate envInteractiveShellInit "interactiveShellInit";
+     })
+
+      (mkIf (!cfg.useBabelfish)
+      {
+        etc."fish/foreign-env/shellInit".source = envShellInit;
+        etc."fish/foreign-env/loginShellInit".source = envLoginShellInit;
+        etc."fish/foreign-env/interactiveShellInit".source = envInteractiveShellInit;
+      })
+
+      {
+        etc."fish/nixos-env-preinit.fish".text =
+        if cfg.useBabelfish
+        then ''
+          # source the NixOS environment config
+          if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]
+            source /etc/fish/setEnvironment.fish
+          end
+        ''
+        else ''
+          # This happens before $__fish_datadir/config.fish sets fish_function_path, so it is currently
+          # unset. We set it and then completely erase it, leaving its configuration to $__fish_datadir/config.fish
+          set fish_function_path ${pkgs.fishPlugins.foreign-env}/share/fish/vendor_functions.d $__fish_datadir/functions
+
+          # source the NixOS environment config
+          if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]
+            fenv source ${config.system.build.setEnvironment}
+          end
+
+          # clear fish_function_path so that it will be correctly set when we return to $__fish_datadir/config.fish
+          set -e fish_function_path
+        '';
+      }
+
+      {
+        etc."fish/config.fish".text = ''
+        # /etc/fish/config.fish: DO NOT EDIT -- this file has been generated automatically.
+
+        # if we haven't sourced the general config, do it
+        if not set -q __fish_nixos_general_config_sourced
+          ${sourceEnv "shellInit"}
+
+          ${cfg.shellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew)
+          set -g __fish_nixos_general_config_sourced 1
+        end
+
+        # if we haven't sourced the login config, do it
+        status --is-login; and not set -q __fish_nixos_login_config_sourced
+        and begin
+          ${sourceEnv "loginShellInit"}
+
+          ${cfg.loginShellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew)
+          set -g __fish_nixos_login_config_sourced 1
+        end
+
+        # if we haven't sourced the interactive config, do it
+        status --is-interactive; and not set -q __fish_nixos_interactive_config_sourced
+        and begin
+          ${fishAliases}
+
+          ${sourceEnv "interactiveShellInit"}
+
+          ${cfg.promptInit}
+          ${cfg.interactiveShellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew,
+          # allowing configuration changes in, e.g, aliases, to propagate)
+          set -g __fish_nixos_interactive_config_sourced 1
+        end
+      '';
+      }
+
+      {
+        etc."fish/generated_completions".source =
+        let
+          patchedGenerator = pkgs.stdenv.mkDerivation {
+            name = "fish_patched-completion-generator";
+            srcs = [
+              "${pkgs.fish}/share/fish/tools/create_manpage_completions.py"
+              "${pkgs.fish}/share/fish/tools/deroff.py"
+            ];
+            unpackCmd = "cp $curSrc $(basename $curSrc)";
+            sourceRoot = ".";
+            patches = [ ./fish_completion-generator.patch ]; # to prevent collisions of identical completion files
+            dontBuild = true;
+            installPhase = ''
+              mkdir -p $out
+              cp * $out/
+            '';
+            preferLocalBuild = true;
+            allowSubstitutes = false;
+          };
+          generateCompletions = package: pkgs.runCommand
+            "${package.name}_fish-completions"
+            (
+              {
+                inherit package;
+                preferLocalBuild = true;
+                allowSubstitutes = false;
+              }
+              // optionalAttrs (package ? meta.priority) { meta.priority = package.meta.priority; }
+            )
+            ''
+              mkdir -p $out
+              if [ -d $package/share/man ]; then
+                find $package/share/man -type f | xargs ${pkgs.python3.interpreter} ${patchedGenerator}/create_manpage_completions.py --directory $out >/dev/null
+              fi
+            '';
+        in
+          pkgs.buildEnv {
+            name = "system_fish-completions";
+            ignoreCollisions = true;
+            paths = map generateCompletions config.environment.systemPackages;
+          };
+      }
+
+      # include programs that bring their own completions
+      {
+        pathsToLink = []
+        ++ optional cfg.vendor.config.enable "/share/fish/vendor_conf.d"
+        ++ optional cfg.vendor.completions.enable "/share/fish/vendor_completions.d"
+        ++ optional cfg.vendor.functions.enable "/share/fish/vendor_functions.d";
+      }
+
+      { systemPackages = [ pkgs.fish ]; }
+
+      {
+        shells = [
+          "/run/current-system/sw/bin/fish"
+          "${pkgs.fish}/bin/fish"
+        ];
+      }
+    ];
 
     programs.fish.interactiveShellInit = ''
       # add completions generated by NixOS to $fish_complete_path
@@ -187,61 +297,6 @@ in
       end
     '';
 
-    environment.etc."fish/generated_completions".source =
-      let
-        patchedGenerator = pkgs.stdenv.mkDerivation {
-          name = "fish_patched-completion-generator";
-          srcs = [
-            "${pkgs.fish}/share/fish/tools/create_manpage_completions.py"
-            "${pkgs.fish}/share/fish/tools/deroff.py"
-          ];
-          unpackCmd = "cp $curSrc $(basename $curSrc)";
-          sourceRoot = ".";
-          patches = [ ./fish_completion-generator.patch ]; # to prevent collisions of identical completion files
-          dontBuild = true;
-          installPhase = ''
-            mkdir -p $out
-            cp * $out/
-          '';
-          preferLocalBuild = true;
-          allowSubstitutes = false;
-        };
-        generateCompletions = package: pkgs.runCommand
-          "${package.name}_fish-completions"
-          (
-            {
-              inherit package;
-              preferLocalBuild = true;
-              allowSubstitutes = false;
-            }
-            // optionalAttrs (package ? meta.priority) { meta.priority = package.meta.priority; }
-          )
-          ''
-            mkdir -p $out
-            if [ -d $package/share/man ]; then
-              find $package/share/man -type f | xargs ${pkgs.python3.interpreter} ${patchedGenerator}/create_manpage_completions.py --directory $out >/dev/null
-            fi
-          '';
-      in
-        pkgs.buildEnv {
-          name = "system_fish-completions";
-          ignoreCollisions = true;
-          paths = map generateCompletions config.environment.systemPackages;
-        };
-
-    # include programs that bring their own completions
-    environment.pathsToLink = []
-      ++ optional cfg.vendor.config.enable "/share/fish/vendor_conf.d"
-      ++ optional cfg.vendor.completions.enable "/share/fish/vendor_completions.d"
-      ++ optional cfg.vendor.functions.enable "/share/fish/vendor_functions.d";
-
-    environment.systemPackages = [ pkgs.fish ];
-
-    environment.shells = [
-      "/run/current-system/sw/bin/fish"
-      "${pkgs.fish}/bin/fish"
-    ];
-
   };
 
 }
diff --git a/nixos/modules/programs/msmtp.nix b/nixos/modules/programs/msmtp.nix
new file mode 100644
index 0000000000000..217060e6b3b32
--- /dev/null
+++ b/nixos/modules/programs/msmtp.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.msmtp;
+
+in {
+  meta.maintainers = with maintainers; [ pacien ];
+
+  options = {
+    programs.msmtp = {
+      enable = mkEnableOption "msmtp - an SMTP client";
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to set the system sendmail to msmtp's.
+        '';
+      };
+
+      defaults = mkOption {
+        type = types.attrs;
+        default = {};
+        example = {
+          aliases = "/etc/aliases";
+          port = 587;
+          tls = true;
+        };
+        description = ''
+          Default values applied to all accounts.
+          See msmtp(1) for the available options.
+        '';
+      };
+
+      accounts = mkOption {
+        type = with types; attrsOf attrs;
+        default = {};
+        example = {
+          "default" = {
+            host = "smtp.example";
+            auth = true;
+            user = "someone";
+            passwordeval = "cat /secrets/password.txt";
+          };
+        };
+        description = ''
+          Named accounts and their respective configurations.
+          The special name "default" allows a default account to be defined.
+          See msmtp(1) for the available options.
+
+          Use `programs.msmtp.extraConfig` instead of this attribute set-based
+          option if ordered account inheritance is needed.
+
+          It is advised to use the `passwordeval` setting to read the password
+          from a secret file to avoid having it written in the world-readable
+          nix store. The password file must end with a newline (`\n`).
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to add to the msmtp configuration verbatim.
+          See msmtp(1) for the syntax and available options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.msmtp ];
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
+      program = "sendmail";
+      source = "${pkgs.msmtp}/bin/sendmail";
+      setuid = false;
+      setgid = false;
+    };
+
+    environment.etc."msmtprc".text = let
+      mkValueString = v:
+        if v == true then "on"
+        else if v == false then "off"
+        else generators.mkValueStringDefault {} v;
+      mkKeyValueString = k: v: "${k} ${mkValueString v}";
+      mkInnerSectionString =
+        attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValueString attrs);
+      mkAccountString = name: attrs: ''
+        account ${name}
+        ${mkInnerSectionString attrs}
+      '';
+    in ''
+      defaults
+      ${mkInnerSectionString cfg.defaults}
+
+      ${concatStringsSep "\n" (mapAttrsToList mkAccountString cfg.accounts)}
+
+      ${cfg.extraConfig}
+    '';
+  };
+}
diff --git a/nixos/modules/programs/proxychains.nix b/nixos/modules/programs/proxychains.nix
new file mode 100644
index 0000000000000..7743f79c1c0a9
--- /dev/null
+++ b/nixos/modules/programs/proxychains.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+
+  cfg = config.programs.proxychains;
+
+  configFile = ''
+    ${cfg.chain.type}_chain
+    ${optionalString (cfg.chain.type == "random")
+    "chain_len = ${builtins.toString cfg.chain.length}"}
+    ${optionalString cfg.proxyDNS "proxy_dns"}
+    ${optionalString cfg.quietMode "quiet_mode"}
+    remote_dns_subnet ${builtins.toString cfg.remoteDNSSubnet}
+    tcp_read_time_out ${builtins.toString cfg.tcpReadTimeOut}
+    tcp_connect_time_out ${builtins.toString cfg.tcpConnectTimeOut}
+    localnet ${cfg.localnet}
+    [ProxyList]
+    ${builtins.concatStringsSep "\n"
+      (lib.mapAttrsToList (k: v: "${v.type} ${v.host} ${builtins.toString v.port}")
+        (lib.filterAttrs (k: v: v.enable) cfg.proxies))}
+  '';
+
+  proxyOptions = {
+    options = {
+      enable = mkEnableOption "this proxy";
+
+      type = mkOption {
+        type = types.enum [ "http" "socks4" "socks5" ];
+        description = "Proxy type.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        description = "Proxy host or IP address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        description = "Proxy port";
+      };
+    };
+  };
+
+in {
+
+  ###### interface
+
+  options = {
+
+    programs.proxychains = {
+
+      enable = mkEnableOption "installing proxychains configuration";
+
+      chain = {
+        type = mkOption {
+          type = types.enum [ "dynamic" "strict" "random" ];
+          default = "strict";
+          description = ''
+            <literal>dynamic</literal> - Each connection will be done via chained proxies
+            all proxies chained in the order as they appear in the list
+            at least one proxy must be online to play in chain
+            (dead proxies are skipped)
+            otherwise <literal>EINTR</literal> is returned to the app.
+
+            <literal>strict</literal> - Each connection will be done via chained proxies
+            all proxies chained in the order as they appear in the list
+            all proxies must be online to play in chain
+            otherwise <literal>EINTR</literal> is returned to the app.
+
+            <literal>random</literal> - Each connection will be done via random proxy
+            (or proxy chain, see <option>programs.proxychains.chain.length</option>) from the list.
+          '';
+        };
+        length = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          description = ''
+            Chain length for random chain.
+          '';
+        };
+      };
+
+      proxyDNS = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Proxy DNS requests - no leak for DNS data.";
+      };
+
+      quietMode = mkEnableOption "Quiet mode (no output from the library).";
+
+      remoteDNSSubnet = mkOption {
+        type = types.enum [ 10 127 224 ];
+        default = 224;
+        description = ''
+          Set the class A subnet number to use for the internal remote DNS mapping, uses the reserved 224.x.x.x range by default.
+        '';
+      };
+
+      tcpReadTimeOut = mkOption {
+        type = types.int;
+        default = 15000;
+        description = "Connection read time-out in milliseconds.";
+      };
+
+      tcpConnectTimeOut = mkOption {
+        type = types.int;
+        default = 8000;
+        description = "Connection time-out in milliseconds.";
+      };
+
+      localnet = mkOption {
+        type = types.str;
+        default = "127.0.0.0/255.0.0.0";
+        description = "By default enable localnet for loopback address ranges.";
+      };
+
+      proxies = mkOption {
+        type = types.attrsOf (types.submodule proxyOptions);
+        description = ''
+          Proxies to be used by proxychains.
+        '';
+
+        example = literalExample ''
+          { myproxy =
+            { type = "socks4";
+              host = "127.0.0.1";
+              port = 1337;
+            };
+          }
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  meta.maintainers = with maintainers; [ sorki ];
+
+  config = mkIf cfg.enable {
+
+    assertions = singleton {
+      assertion = cfg.chain.type != "random" && cfg.chain.length == null;
+      message = ''
+        Option `programs.proxychains.chain.length`
+        only makes sense with `programs.proxychains.chain.type` = "random".
+      '';
+    };
+
+    programs.proxychains.proxies = mkIf config.services.tor.client.enable
+      {
+        torproxy = mkDefault {
+          enable = true;
+          type = "socks4";
+          host = "127.0.0.1";
+          port = 9050;
+        };
+      };
+
+    environment.etc."proxychains.conf".text = configFile;
+    environment.systemPackages = [ pkgs.proxychains ];
+  };
+
+}
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 40af4d0ff5aeb..d4a7769bbd6d6 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -36,7 +36,7 @@ in
       askPassword = mkOption {
         type = types.str;
         default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass";
-        description = ''Program used by SSH to ask for passwords.'';
+        description = "Program used by SSH to ask for passwords.";
       };
 
       forwardX11 = mkOption {
diff --git a/nixos/modules/programs/ssmtp.nix b/nixos/modules/programs/ssmtp.nix
index 15d2750c193ff..8039763faccc9 100644
--- a/nixos/modules/programs/ssmtp.nix
+++ b/nixos/modules/programs/ssmtp.nix
@@ -1,6 +1,6 @@
 # Configuration for `ssmtp', a trivial mail transfer agent that can
 # replace sendmail/postfix on simple systems.  It delivers email
-# directly to an SMTP server defined in its configuration file, wihout
+# directly to an SMTP server defined in its configuration file, without
 # queueing mail locally.
 
 { config, lib, pkgs, ... }:
@@ -142,6 +142,13 @@ in
 
   config = mkIf cfg.enable {
 
+    assertions = [
+      {
+        assertion = cfg.useSTARTTLS -> cfg.useTLS;
+        message = "services.ssmtp.useSTARTTLS has no effect without services.ssmtp.useTLS";
+      }
+    ];
+
     services.ssmtp.settings = mkMerge [
       ({
         MailHub = cfg.hostName;
@@ -155,15 +162,16 @@ in
       (mkIf (cfg.authPassFile != null) { AuthPassFile = cfg.authPassFile; })
     ];
 
-    environment.etc."ssmtp/ssmtp.conf".source =
-      let
-        toStr = value:
+    # careful here: ssmtp REQUIRES all config lines to end with a newline char!
+    environment.etc."ssmtp/ssmtp.conf".text = with generators; toKeyValue {
+      mkKeyValue = mkKeyValueDefault {
+        mkValueString = value:
           if value == true then "YES"
           else if value == false then "NO"
-          else builtins.toString value
+          else mkValueStringDefault {} value
         ;
-      in
-        pkgs.writeText "ssmtp.conf" (concatStringsSep "\n" (mapAttrsToList (key: value: "${key}=${toStr value}") cfg.settings));
+      } "=";
+    } cfg.settings;
 
     environment.systemPackages = [pkgs.ssmtp];
 
diff --git a/nixos/modules/programs/tilp2.nix b/nixos/modules/programs/tilp2.nix
new file mode 100644
index 0000000000000..da9e32e3e6c6d
--- /dev/null
+++ b/nixos/modules/programs/tilp2.nix
@@ -0,0 +1,28 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.tilp2;
+
+in {
+  options.programs.tilp2 = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable tilp2 and udev rules for supported calculators.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [
+      pkgs.libticables2
+    ];
+
+    environment.systemPackages = [
+      pkgs.tilp2
+    ];
+  };
+}
diff --git a/nixos/modules/programs/venus.nix b/nixos/modules/programs/venus.nix
deleted file mode 100644
index 58faf38777d06..0000000000000
--- a/nixos/modules/programs/venus.nix
+++ /dev/null
@@ -1,173 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.venus;
-
-  configFile = pkgs.writeText "venus.ini"
-    ''
-      [Planet]
-      name = ${cfg.name}
-      link = ${cfg.link}
-      owner_name = ${cfg.ownerName}
-      owner_email = ${cfg.ownerEmail}
-      output_theme = ${cfg.cacheDirectory}/theme
-      output_dir = ${cfg.outputDirectory}
-      cache_directory = ${cfg.cacheDirectory}
-      items_per_page = ${toString cfg.itemsPerPage}
-      ${(concatStringsSep "\n\n"
-            (map ({ name, feedUrl, homepageUrl }:
-            ''
-              [${feedUrl}]
-              name = ${name}
-              link = ${homepageUrl}
-            '') cfg.feeds))}
-    '';
-
-in
-{
-
-  options = {
-    services.venus = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Planet Venus is an awesome ‘river of news’ feed reader. It downloads
-          news feeds published by web sites and aggregates their content
-          together into a single combined feed, latest news first.
-        '';
-      };
-
-      dates = mkOption {
-        default = "*:0/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 Venus will collect feeds.
-        '';
-      };
-
-      user = mkOption {
-        default = "root";
-        type = types.str;
-        description = ''
-          User for running venus script.
-        '';
-      };
-
-      group = mkOption {
-        default = "root";
-        type = types.str;
-        description = ''
-          Group for running venus script.
-        '';
-      };
-
-      name = mkOption {
-        default = "NixOS Planet";
-        type = types.str;
-        description = ''
-          Your planet's name.
-        '';
-      };
-
-      link = mkOption {
-        default = "https://planet.nixos.org";
-        type = types.str;
-        description = ''
-          Link to the main page.
-        '';
-      };
-
-      ownerName = mkOption {
-        default = "Rok Garbas";
-        type = types.str;
-        description = ''
-          Your name.
-        '';
-      };
-
-      ownerEmail = mkOption {
-        default = "some@example.com";
-        type = types.str;
-        description = ''
-          Your e-mail address.
-        '';
-      };
-
-      outputTheme = mkOption {
-        default = "${pkgs.venus}/themes/classic_fancy";
-        type = types.path;
-        description = ''
-          Directory containing a config.ini file which is merged with this one.
-          This is typically used to specify templating and bill of material
-          information.
-        '';
-      };
-
-      outputDirectory = mkOption {
-        type = types.path;
-        description = ''
-          Directory to place output files.
-        '';
-      };
-
-      cacheDirectory = mkOption {
-        default = "/var/cache/venus";
-        type = types.path;
-        description = ''
-            Where cached feeds are stored.
-        '';
-      };
-
-      itemsPerPage = mkOption {
-        default = 15;
-        type = types.int;
-        description = ''
-          How many items to put on each page.
-        '';
-      };
-
-      feeds = mkOption {
-        default = [];
-        example = [
-          {
-            name = "Rok Garbas";
-            feedUrl= "http://url/to/rss/feed.xml";
-            homepageUrl = "http://garbas.si";
-          }
-        ];
-        description = ''
-          List of feeds.
-        '';
-      };
-
-    };
-  };
-
-  config = mkIf cfg.enable {
-
-    system.activationScripts.venus =
-      ''
-        mkdir -p ${cfg.outputDirectory}
-        chown ${cfg.user}:${cfg.group} ${cfg.outputDirectory} -R
-        rm -rf ${cfg.cacheDirectory}/theme
-        mkdir -p ${cfg.cacheDirectory}/theme
-        cp -R ${cfg.outputTheme}/* ${cfg.cacheDirectory}/theme
-        chown ${cfg.user}:${cfg.group} ${cfg.cacheDirectory} -R
-      '';
-
-    systemd.services.venus =
-      { description = "Planet Venus Feed Reader";
-        path  = [ pkgs.venus ];
-        script = "exec venus-planet ${configFile}";
-        serviceConfig.User = "${cfg.user}";
-        serviceConfig.Group = "${cfg.group}";
-        startAt = cfg.dates;
-      };
-
-  };
-}
diff --git a/nixos/modules/programs/vim.nix b/nixos/modules/programs/vim.nix
index fe0e7f2c6d6b9..9f46dff2a2931 100644
--- a/nixos/modules/programs/vim.nix
+++ b/nixos/modules/programs/vim.nix
@@ -14,10 +14,20 @@ in {
         using the EDITOR environment variable.
       '';
     };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.vim;
+      defaultText = "pkgs.vim";
+      example = "pkgs.vimHugeX";
+      description = ''
+        vim package to use.
+      '';
+    };
   };
 
   config = mkIf cfg.defaultEditor {
-        environment.systemPackages = [ pkgs.vim ];
-        environment.variables = { EDITOR = mkOverride 900 "vim"; };
+    environment.systemPackages = [ cfg.package ];
+    environment.variables = { EDITOR = mkOverride 900 "vim"; };
   };
 }
diff --git a/nixos/modules/programs/wshowkeys.nix b/nixos/modules/programs/wshowkeys.nix
new file mode 100644
index 0000000000000..09b008af1d5db
--- /dev/null
+++ b/nixos/modules/programs/wshowkeys.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.wshowkeys;
+in {
+  meta.maintainers = with maintainers; [ primeos ];
+
+  options = {
+    programs.wshowkeys = {
+      enable = mkEnableOption ''
+        wshowkeys (displays keypresses on screen on supported Wayland
+        compositors). It requires root permissions to read input events, but
+        these permissions are dropped after startup'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.wshowkeys.source = "${pkgs.wshowkeys}/bin/wshowkeys";
+  };
+}
diff --git a/nixos/modules/programs/x2goserver.nix b/nixos/modules/programs/x2goserver.nix
index 7d74231e956b7..05707a56542f7 100644
--- a/nixos/modules/programs/x2goserver.nix
+++ b/nixos/modules/programs/x2goserver.nix
@@ -110,7 +110,7 @@ in {
       "L+ /usr/local/bin/chmod - - - - ${coreutils}/bin/chmod"
       "L+ /usr/local/bin/cp - - - - ${coreutils}/bin/cp"
       "L+ /usr/local/bin/sed - - - - ${gnused}/bin/sed"
-      "L+ /usr/local/bin/setsid - - - - ${utillinux}/bin/setsid"
+      "L+ /usr/local/bin/setsid - - - - ${util-linux}/bin/setsid"
       "L+ /usr/local/bin/xrandr - - - - ${xorg.xrandr}/bin/xrandr"
       "L+ /usr/local/bin/xmodmap - - - - ${xorg.xmodmap}/bin/xmodmap"
     ];
diff --git a/nixos/modules/programs/xss-lock.nix b/nixos/modules/programs/xss-lock.nix
index 83ed713864079..ceb7259b3d779 100644
--- a/nixos/modules/programs/xss-lock.nix
+++ b/nixos/modules/programs/xss-lock.nix
@@ -11,7 +11,7 @@ in
 
     lockerCommand = mkOption {
       default = "${pkgs.i3lock}/bin/i3lock";
-      example = literalExample ''''${pkgs.i3lock-fancy}/bin/i3lock-fancy'';
+      example = literalExample "\${pkgs.i3lock-fancy}/bin/i3lock-fancy";
       type = types.separatedString " ";
       description = "Locker to be used with xsslock";
     };
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index a87db475e012d..2d07e421efe45 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -32,6 +32,7 @@ with lib;
     (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" "frab" ] "The frab 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: " +
@@ -69,6 +70,8 @@ with lib;
     '')
 
     (mkRemovedOptionModule [ "services" "seeks" ] "")
+    (mkRemovedOptionModule [ "services" "venus" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "flashpolicyd" ] "The flashpolicyd module has been removed. Adobe Flash Player is deprecated.")
 
     # 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 31fbc36147446..c33a92580d4cd 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -7,6 +7,11 @@ let
   numCerts = length (builtins.attrNames cfg.certs);
   _24hSecs = 60 * 60 * 24;
 
+  # Used to make unique paths for each cert/account config set
+  mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
+  mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
+  accountDirRoot = "/var/lib/acme/.lego/accounts/";
+
   # There are many services required to make cert renewals work.
   # They all follow a common structure:
   #   - They inherit this commonServiceConfig
@@ -19,7 +24,7 @@ let
       Type = "oneshot";
       User = "acme";
       Group = mkDefault "acme";
-      UMask = 0027;
+      UMask = 0023;
       StateDirectoryMode = 750;
       ProtectSystem = "full";
       PrivateTmp = true;
@@ -54,23 +59,35 @@ let
     '';
   };
 
-  # Previously, all certs were owned by whatever user was configured in
-  # config.security.acme.certs.<cert>.user. Now everything is owned by and
-  # run by the acme user.
-  userMigrationService = {
-    description = "Fix owner and group of all ACME certificates";
-
-    script = with builtins; concatStringsSep "\n" (mapAttrsToList (cert: data: ''
-      for fixpath in /var/lib/acme/${escapeShellArg cert} /var/lib/acme/.lego/${escapeShellArg cert}; do
+  # Ensures that directories which are shared across all certs
+  # exist and have the correct user and group, since group
+  # is configurable on a per-cert basis.
+  userMigrationService = let
+    script = with builtins; ''
+      chown -R acme .lego/accounts
+    '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
+      for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
         if [ -d "$fixpath" ]; then
-          chmod -R 750 "$fixpath"
+          chmod -R u=rwX,g=rX,o= "$fixpath"
           chown -R acme:${data.group} "$fixpath"
         fi
       done
-    '') certConfigs);
+    '') certConfigs));
+  in {
+    description = "Fix owner and group of all ACME certificates";
+
+    serviceConfig = commonServiceConfig // {
+      # We don't want this to run every time a renewal happens
+      RemainAfterExit = true;
 
-    # We don't want this to run every time a renewal happens
-    serviceConfig.RemainAfterExit = true;
+      # These StateDirectory entries negate the need for tmpfiles
+      StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
+      StateDirectoryMode = 755;
+      WorkingDirectory = "/var/lib/acme";
+
+      # Run the start script as root
+      ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
+    };
   };
 
   certToConfig = cert: data: let
@@ -101,11 +118,10 @@ let
       ${toString acmeServer} ${toString data.dnsProvider}
       ${toString data.ocspMustStaple} ${data.keyType}
     '';
-    mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
     certDir = mkHash hashData;
     domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
-    othersHash = mkHash "${toString acmeServer} ${data.keyType}";
-    accountDir = "/var/lib/acme/.lego/accounts/" + othersHash;
+    accountHash = (mkAccountHash acmeServer data);
+    accountDir = accountDirRoot + accountHash;
 
     protocolOpts = if useDns then (
       [ "--dns" data.dnsProvider ]
@@ -122,26 +138,28 @@ let
       "--email" data.email
       "--key-type" data.keyType
     ] ++ protocolOpts
-      ++ optionals data.ocspMustStaple [ "--must-staple" ]
       ++ optionals (acmeServer != null) [ "--server" acmeServer ]
       ++ concatMap (name: [ "-d" name ]) extraDomains
       ++ data.extraLegoFlags;
 
+    # Although --must-staple is common to both modes, it is not declared as a
+    # mode-agnostic argument in lego and thus must come after the mode.
     runOpts = escapeShellArgs (
       commonOpts
       ++ [ "run" ]
+      ++ optionals data.ocspMustStaple [ "--must-staple" ]
       ++ data.extraLegoRunFlags
     );
     renewOpts = escapeShellArgs (
       commonOpts
       ++ [ "renew" "--reuse-key" ]
+      ++ optionals data.ocspMustStaple [ "--must-staple" ]
       ++ data.extraLegoRenewFlags
     );
 
   in {
-    inherit accountDir selfsignedDeps;
+    inherit accountHash cert selfsignedDeps;
 
-    webroot = data.webroot;
     group = data.group;
 
     renewTimer = {
@@ -181,7 +199,10 @@ let
 
         StateDirectory = "acme/${cert}";
 
-        BindPaths = "/var/lib/acme/.minica:/tmp/ca /var/lib/acme/${cert}:/tmp/${keyName}";
+        BindPaths = [
+          "/var/lib/acme/.minica:/tmp/ca"
+          "/var/lib/acme/${cert}:/tmp/${keyName}"
+        ];
       };
 
       # Working directory will be /tmp
@@ -219,16 +240,22 @@ let
       serviceConfig = commonServiceConfig // {
         Group = data.group;
 
-        # AccountDir dir will be created by tmpfiles to ensure correct permissions
-        # And to avoid deletion during systemctl clean
-        # acme/.lego/${cert} is listed so that it is deleted during systemctl clean
-        StateDirectory = "acme/${cert} acme/.lego/${cert} acme/.lego/${cert}/${certDir}";
+        # Keep in mind that these directories will be deleted if the user runs
+        # systemctl clean --what=state
+        # acme/.lego/${cert} is listed for this reason.
+        StateDirectory = [
+          "acme/${cert}"
+          "acme/.lego/${cert}"
+          "acme/.lego/${cert}/${certDir}"
+          "acme/.lego/accounts/${accountHash}"
+        ];
 
         # Needs to be space separated, but can't use a multiline string because that'll include newlines
-        BindPaths =
-          "${accountDir}:/tmp/accounts " +
-          "/var/lib/acme/${cert}:/tmp/out " +
-          "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates ";
+        BindPaths = [
+          "${accountDir}:/tmp/accounts"
+          "/var/lib/acme/${cert}:/tmp/out"
+          "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
+        ];
 
         # Only try loading the credentialsFile if the dns challenge is enabled
         EnvironmentFile = mkIf useDns data.credentialsFile;
@@ -245,12 +272,18 @@ let
 
       # Working directory will be /tmp
       script = ''
-        set -euo pipefail
+        set -euxo pipefail
+
+        ${optionalString (data.webroot != null) ''
+          # Ensure the webroot exists
+          mkdir -p '${data.webroot}/.well-known/acme-challenge'
+          chown 'acme:${data.group}' ${data.webroot}/{.well-known,.well-known/acme-challenge}
+        ''}
 
         echo '${domainHash}' > domainhash.txt
 
         # Check if we can renew
-        if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' ]; then
+        if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
 
           # When domains are updated, there's no need to do a full
           # Lego run, but it's likely renew won't work if days is too low.
@@ -268,7 +301,7 @@ let
 
         mv domainhash.txt certificates/
         chmod 640 certificates/*
-        chmod -R 700 accounts/*
+        chmod -R u=rwX,g=,o= accounts/*
 
         # Group might change between runs, re-apply it
         chown 'acme:${data.group}' certificates/*
@@ -313,7 +346,7 @@ let
       webroot = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/acme/acme-challenges";
+        example = "/var/lib/acme/acme-challenge";
         description = ''
           Where the webroot of the HTTP vhost is located.
           <filename>.well-known/acme-challenge/</filename> directory
@@ -546,12 +579,12 @@ in {
         example = literalExample ''
           {
             "example.com" = {
-              webroot = "/var/www/challenges/";
+              webroot = "/var/lib/acme/acme-challenge/";
               email = "foo@example.com";
               extraDomainNames = [ "www.example.com" "foo.example.com" ];
             };
             "bar.example.com" = {
-              webroot = "/var/www/challenges/";
+              webroot = "/var/lib/acme/acme-challenge/";
               email = "bar@example.com";
             };
           }
@@ -660,21 +693,33 @@ in {
 
       systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
 
-      # .lego and .lego/accounts specified to fix any incorrect permissions
-      systemd.tmpfiles.rules = [
-        "d /var/lib/acme/.lego - acme acme"
-        "d /var/lib/acme/.lego/accounts - acme acme"
-      ] ++ (unique (concatMap (conf: [
-          "d ${conf.accountDir} - acme acme"
-        ] ++ (optional (conf.webroot != null) "d ${conf.webroot}/.well-known/acme-challenge - acme ${conf.group}")
-      ) (attrValues certConfigs)));
-
-      # Create some targets which can be depended on to be "active" after cert renewals
-      systemd.targets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
-        wantedBy = [ "default.target" ];
-        requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
-        after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
-      }) certConfigs;
+      systemd.targets = let
+        # Create some targets which can be depended on to be "active" after cert renewals
+        finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
+          wantedBy = [ "default.target" ];
+          requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
+          after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
+        }) certConfigs;
+
+        # Create targets to limit the number of simultaneous account creations
+        # How it works:
+        # - Pick a "leader" cert service, which will be in charge of creating the account,
+        #   and run first (requires + after)
+        # - Make all other cert services sharing the same account wait for the leader to
+        #   finish before starting (requiredBy + before).
+        # Using a target here is fine - account creation is a one time event. Even if
+        # systemd clean --what=state is used to delete the account, so long as the user
+        # then runs one of the cert services, there won't be any issues.
+        accountTargets = mapAttrs' (hash: confs: let
+          leader = "acme-${(builtins.head confs).cert}.service";
+          dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
+        in nameValuePair "acme-account-${hash}" {
+          requiredBy = dependantServices;
+          before = dependantServices;
+          requires = [ leader ];
+          after = [ leader ];
+        }) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
+      in finishedTargets // accountTargets;
     })
   ];
 
diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml
index 17e94bc12fb21..a78ff05f2eaad 100644
--- a/nixos/modules/security/acme.xml
+++ b/nixos/modules/security/acme.xml
@@ -115,15 +115,18 @@ services.nginx = {
 <programlisting>
 <xref linkend="opt-security.acme.acceptTerms" /> = true;
 <xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
+
+# /var/lib/acme/.challenges must be writable by the ACME user
+# and readable by the Nginx user. The easiest way to achieve
+# this is to add the Nginx user to the ACME group.
+<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ];
+
 services.nginx = {
   <link linkend="opt-services.nginx.enable">enable</link> = true;
   <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
     "acmechallenge.example.com" = {
       # Catchall vhost, will redirect users to HTTPS for all vhosts
       <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
-      # /var/lib/acme/.challenges must be writable by the ACME user
-      # and readable by the Nginx user.
-      # By default, this is the case.
       locations."/.well-known/acme-challenge" = {
         <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges";
       };
@@ -134,6 +137,7 @@ services.nginx = {
   };
 }
 # Alternative config for Apache
+<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ];
 services.httpd = {
   <link linkend="opt-services.httpd.enable">enable = true;</link>
   <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = {
@@ -162,6 +166,9 @@ services.httpd = {
 <xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
   <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
   <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
+  # Ensure that the web server you use can read the generated certs
+  # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
+  <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
   # Since we have a wildcard vhost to handle port 80,
   # we can generate certs for anything!
   # Just make sure your DNS resolves them.
@@ -257,10 +264,35 @@ chmod 400 /var/lib/secrets/certs.secret
   <para>
    Should you need to regenerate a particular certificate in a hurry, such
    as when a vulnerability is found in Let's Encrypt, there is now a convenient
-   mechanism for doing so. Running <literal>systemctl clean acme-example.com.service</literal>
-   will remove all certificate files for the given domain, allowing you to then
-   <literal>systemctl start acme-example.com.service</literal> to generate fresh
-   ones.
+   mechanism for doing so. Running
+   <literal>systemctl clean --what=state acme-example.com.service</literal>
+   will remove all certificate files and the account data for the given domain,
+   allowing you to then <literal>systemctl start acme-example.com.service</literal>
+   to generate fresh ones.
   </para>
  </section>
+ <section xml:id="module-security-acme-fix-jws">
+  <title>Fixing JWS Verification error</title>
+
+  <para>
+   It is possible that your account credentials file may become corrupt and need
+   to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>.
+   The solution is to simply delete the associated accounts file and
+   re-run the affected service(s).
+  </para>
+
+<programlisting>
+# Find the accounts folder for the certificate
+systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
+export accountdir="$(!!)"
+# Move this folder to some place else
+mv /var/lib/acme/.lego/$accountdir{,.bak}
+# Recreate the folder using systemd-tmpfiles
+systemd-tmpfiles --create
+# Get a new account and reissue certificates
+# Note: Do this for all certs that share the same account email address
+systemctl start acme-example.com.service
+</programlisting>
+
+ </section>
 </chapter>
diff --git a/nixos/modules/security/doas.nix b/nixos/modules/security/doas.nix
index b81f2d0c2d520..27f6870aaf374 100644
--- a/nixos/modules/security/doas.nix
+++ b/nixos/modules/security/doas.nix
@@ -12,6 +12,7 @@ let
 
   mkOpts = rule: concatStringsSep " " [
     (optionalString rule.noPass "nopass")
+    (optionalString rule.noLog "nolog")
     (optionalString rule.persist "persist")
     (optionalString rule.keepEnv "keepenv")
     "setenv { SSH_AUTH_SOCK ${concatStringsSep " " rule.setEnv} }"
@@ -118,6 +119,16 @@ in
               '';
             };
 
+            noLog = mkOption {
+              type = with types; bool;
+              default = false;
+              description = ''
+                If <code>true</code>, successful executions will not be logged
+                to
+                <citerefentry><refentrytitle>syslogd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+              '';
+            };
+
             persist = mkOption {
               type = with types; bool;
               default = false;
diff --git a/nixos/modules/security/hidepid.nix b/nixos/modules/security/hidepid.nix
index 55a48ea3c9c62..4953f517e93be 100644
--- a/nixos/modules/security/hidepid.nix
+++ b/nixos/modules/security/hidepid.nix
@@ -23,5 +23,9 @@ with lib;
 
     boot.specialFileSystems."/proc".options = [ "hidepid=2" "gid=${toString config.ids.gids.proc}" ];
     systemd.services.systemd-logind.serviceConfig.SupplementaryGroups = [ "proc" ];
+
+    # Disable cgroupsv2, which doesn't work with hidepid.
+    # https://github.com/NixOS/nixpkgs/pull/104094#issuecomment-729996203
+    systemd.enableUnifiedCgroupHierarchy = false;
   };
 }
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index a20d0a243a8ef..103cf20501237 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -318,6 +318,42 @@ let
         '';
       };
 
+      gnupg = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If enabled, pam_gnupg will attempt to automatically unlock the
+            user's GPG keys with the login password via
+            <command>gpg-agent</command>. The keygrips of all keys to be
+            unlocked should be written to <filename>~/.pam-gnupg</filename>,
+            and can be queried with <command>gpg -K --with-keygrip</command>.
+            Presetting passphrases must be enabled by adding
+            <literal>allow-preset-passphrase</literal> in
+            <filename>~/.gnupg/gpg-agent.conf</filename>.
+          '';
+        };
+
+        noAutostart = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Don't start <command>gpg-agent</command> if it is not running.
+            Useful in conjunction with starting <command>gpg-agent</command> as
+            a systemd user service.
+          '';
+        };
+
+        storeOnly = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Don't send the password immediately after login, but store for PAM
+            <literal>session</literal>.
+          '';
+        };
+      };
+
       text = mkOption {
         type = types.nullOr types.lines;
         description = "Contents of the PAM service file.";
@@ -358,9 +394,9 @@ let
           ${optionalString cfg.requireWheel
               "auth required pam_wheel.so use_uid"}
           ${optionalString cfg.logFailures
-              "auth required pam_tally.so"}
+              "auth required pam_faillock.so"}
           ${optionalString (config.security.pam.enableSSHAgentAuth && cfg.sshAgentAuth)
-              "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"}
+              "auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=${lib.concatStringsSep ":" config.services.openssh.authorizedKeysFiles}"}
           ${optionalString cfg.fprintAuth
               "auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so"}
           ${let p11 = config.security.pam.p11; in optionalString cfg.p11Auth
@@ -386,6 +422,7 @@ let
             || cfg.enableKwallet
             || cfg.enableGnomeKeyring
             || cfg.googleAuthenticator.enable
+            || cfg.gnupg.enable
             || cfg.duoSecurity.enable)) ''
               auth required pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth
               ${optionalString config.security.pam.enableEcryptfs
@@ -393,10 +430,14 @@ let
               ${optionalString cfg.pamMount
                 "auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so"}
               ${optionalString cfg.enableKwallet
-                ("auth optional ${pkgs.plasma5.kwallet-pam}/lib/security/pam_kwallet5.so" +
-                 " kwalletd=${pkgs.kdeFrameworks.kwallet.bin}/bin/kwalletd5")}
+                ("auth optional ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so" +
+                 " kwalletd=${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5")}
               ${optionalString cfg.enableGnomeKeyring
                 "auth optional ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so"}
+              ${optionalString cfg.gnupg.enable
+                "auth optional ${pkgs.pam_gnupg}/lib/security/pam_gnupg.so"
+                + optionalString cfg.gnupg.storeOnly " store-only"
+               }
               ${optionalString cfg.googleAuthenticator.enable
                 "auth required ${pkgs.googleAuthenticator}/lib/security/pam_google_authenticator.so no_increment_hotp"}
               ${optionalString cfg.duoSecurity.enable
@@ -468,10 +509,14 @@ let
           ${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)
-              ("session optional ${pkgs.plasma5.kwallet-pam}/lib/security/pam_kwallet5.so" +
-               " kwalletd=${pkgs.kdeFrameworks.kwallet.bin}/bin/kwalletd5")}
+              ("session optional ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so" +
+               " kwalletd=${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5")}
           ${optionalString (cfg.enableGnomeKeyring)
               "session optional ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so auto_start"}
+          ${optionalString cfg.gnupg.enable
+              "session optional ${pkgs.pam_gnupg}/lib/security/pam_gnupg.so"
+              + optionalString cfg.gnupg.noAutostart " no-autostart"
+           }
           ${optionalString (config.virtualisation.lxc.lxcfs.enable)
                "session optional ${pkgs.lxc}/lib/security/pam_cgfs.so -c all"}
         '');
diff --git a/nixos/modules/security/pam_mount.nix b/nixos/modules/security/pam_mount.nix
index 77e22a96b553c..9a0143c155c57 100644
--- a/nixos/modules/security/pam_mount.nix
+++ b/nixos/modules/security/pam_mount.nix
@@ -39,8 +39,16 @@ in
     environment.etc."security/pam_mount.conf.xml" = {
       source =
         let
-          extraUserVolumes = filterAttrs (n: u: u.cryptHomeLuks != null) config.users.users;
-          userVolumeEntry = user: "<volume user=\"${user.name}\" path=\"${user.cryptHomeLuks}\" mountpoint=\"${user.home}\" />\n";
+          extraUserVolumes = filterAttrs (n: u: u.cryptHomeLuks != null || u.pamMount != {}) config.users.users;
+          mkAttr = k: v: ''${k}="${v}"'';
+          userVolumeEntry = user: let
+            attrs = {
+              user = user.name;
+              path = user.cryptHomeLuks;
+              mountpoint = user.home;
+            } // user.pamMount;
+          in
+            "<volume ${concatStringsSep " " (mapAttrsToList mkAttr attrs)} />\n";
         in
          pkgs.writeText "pam_mount.conf.xml" ''
           <?xml version="1.0" encoding="utf-8" ?>
@@ -52,7 +60,7 @@ in
           <!-- if activated, requires ofl from hxtools to be present -->
           <logout wait="0" hup="no" term="no" kill="no" />
           <!-- set PATH variable for pam_mount module -->
-          <path>${pkgs.utillinux}/bin</path>
+          <path>${pkgs.util-linux}/bin</path>
           <!-- create mount point if not present -->
           <mkmountpoint enable="1" remove="true" />
 
diff --git a/nixos/modules/security/rngd.nix b/nixos/modules/security/rngd.nix
index cb885c4762d06..8cca1c26d683a 100644
--- a/nixos/modules/security/rngd.nix
+++ b/nixos/modules/security/rngd.nix
@@ -1,56 +1,16 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
+{ lib, ... }:
 let
-  cfg = config.security.rngd;
+  removed = k: lib.mkRemovedOptionModule [ "security" "rngd" k ];
 in
 {
-  options = {
-    security.rngd = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable the rng daemon.  Devices that the kernel recognises
-          as entropy sources are handled automatically by krngd.
-        '';
-      };
-      debug = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable debug output (-d).";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.rngd = {
-      bindsTo = [ "dev-random.device" ];
-
-      after = [ "dev-random.device" ];
-
-      # Clean shutdown without DefaultDependencies
-      conflicts = [ "shutdown.target" ];
-      before = [
-        "sysinit.target"
-        "shutdown.target"
-      ];
-
-      description = "Hardware RNG Entropy Gatherer Daemon";
-
-      # rngd may have to start early to avoid entropy starvation during boot with encrypted swap
-      unitConfig.DefaultDependencies = false;
-      serviceConfig = {
-        ExecStart = "${pkgs.rng-tools}/sbin/rngd -f"
-          + optionalString cfg.debug " -d";
-        # PrivateTmp would introduce a circular dependency if /tmp is on tmpfs and swap is encrypted,
-        # thus depending on rngd before swap, while swap depends on rngd to avoid entropy starvation.
-        NoNewPrivileges = true;
-        PrivateNetwork = true;
-        ProtectSystem = "full";
-        ProtectHome = true;
-      };
-    };
-  };
+  imports = [
+    (removed "enable" ''
+       rngd is not necessary for any device that the kernel recognises
+       as an hardware RNG, as it will automatically run the krngd task
+       to periodically collect random data from the device and mix it
+       into the kernel's RNG.
+    '')
+    (removed "debug"
+      "The rngd module was removed, so its debug option does nothing.")
+  ];
 }
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index 2927d424a8a2e..afb81a2b56be5 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -160,7 +160,7 @@ in {
               + " the 'users.users' option instead as this combination is"
               + " currently not supported.";
     }
-    { assertion = !cfg.serviceConfig.ProtectSystem or false;
+    { assertion = cfg.serviceConfig ? ProtectSystem -> cfg.serviceConfig.ProtectSystem == false;
       message = "${whatOpt "ProtectSystem"}. ProtectSystem is not compatible"
               + " with service confinement as it fails to remount /usr within"
               + " our chroot. Please disable the option.";
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix
index 52de21bca9bfd..3cbf22fea7a96 100644
--- a/nixos/modules/security/wrappers/default.nix
+++ b/nixos/modules/security/wrappers/default.nix
@@ -10,16 +10,8 @@ let
       (n: v: (if v ? program then v else v // {program=n;}))
       wrappers);
 
-  securityWrapper = pkgs.stdenv.mkDerivation {
-    name            = "security-wrapper";
-    phases          = [ "installPhase" "fixupPhase" ];
-    buildInputs     = [ pkgs.libcap pkgs.libcap_ng pkgs.linuxHeaders ];
-    hardeningEnable = [ "pie" ];
-    installPhase = ''
-      mkdir -p $out/bin
-      $CC -Wall -O2 -DWRAPPER_DIR=\"${parentWrapperDir}\" \
-          -lcap-ng -lcap ${./wrapper.c} -o $out/bin/security-wrapper
-    '';
+  securityWrapper = pkgs.callPackage ./wrapper.nix {
+    inherit parentWrapperDir;
   };
 
   ###### Activation script for the setcap wrappers
@@ -163,8 +155,8 @@ in
       # 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";
+      mount.source = "${lib.getBin pkgs.util-linux}/bin/mount";
+      umount.source = "${lib.getBin pkgs.util-linux}/bin/umount";
     };
 
     boot.specialFileSystems.${parentWrapperDir} = {
diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c
index 494e9e93ac222..529669facda87 100644
--- a/nixos/modules/security/wrappers/wrapper.c
+++ b/nixos/modules/security/wrappers/wrapper.c
@@ -4,15 +4,17 @@
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/xattr.h>
 #include <fcntl.h>
 #include <dirent.h>
 #include <assert.h>
 #include <errno.h>
 #include <linux/capability.h>
-#include <sys/capability.h>
 #include <sys/prctl.h>
 #include <limits.h>
-#include <cap-ng.h>
+#include <stdint.h>
+#include <syscall.h>
+#include <byteswap.h>
 
 // Make sure assertions are not compiled out, we use them to codify
 // invariants about this program and we want it to fail fast and
@@ -23,182 +25,172 @@ extern char **environ;
 
 // The WRAPPER_DIR macro is supplied at compile time so that it cannot
 // be changed at runtime
-static char * wrapperDir = WRAPPER_DIR;
+static char *wrapper_dir = WRAPPER_DIR;
 
 // Wrapper debug variable name
-static char * wrapperDebug = "WRAPPER_DEBUG";
-
-// Update the capabilities of the running process to include the given
-// capability in the Ambient set.
-static void set_ambient_cap(cap_value_t cap)
-{
-    capng_get_caps_process();
-
-    if (capng_update(CAPNG_ADD, CAPNG_INHERITABLE, (unsigned long) cap))
-    {
-        perror("cannot raise the capability into the Inheritable set\n");
-        exit(1);
+static char *wrapper_debug = "WRAPPER_DEBUG";
+
+#define CAP_SETPCAP 8
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define LE32_TO_H(x) bswap_32(x)
+#else
+#define LE32_TO_H(x) (x)
+#endif
+
+int get_last_cap(unsigned *last_cap) {
+    FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
+    if (file == NULL) {
+        int saved_errno = errno;
+        fprintf(stderr, "failed to open /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
+        return -saved_errno;
     }
-
-    capng_apply(CAPNG_SELECT_CAPS);
-    
-    if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0))
-    {
-        perror("cannot raise the capability into the Ambient set\n");
-        exit(1);
+    int res = fscanf(file, "%u", last_cap);
+    if (res == EOF) {
+        int saved_errno = errno;
+        fprintf(stderr, "could not read number from /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
+        return -saved_errno;
     }
+    fclose(file);
+    return 0;
 }
 
 // Given the path to this program, fetch its configured capability set
 // (as set by `setcap ... /path/to/file`) and raise those capabilities
 // into the Ambient set.
-static int make_caps_ambient(const char *selfPath)
-{
-    cap_t caps = cap_get_file(selfPath);
+static int make_caps_ambient(const char *self_path) {
+    struct vfs_ns_cap_data data = {};
+    int r = getxattr(self_path, "security.capability", &data, sizeof(data));
+
+    if (r < 0) {
+        if (errno == ENODATA) {
+            // no capabilities set
+            return 0;
+        }
+        fprintf(stderr, "cannot get capabilities for %s: %s", self_path, strerror(errno));
+        return 1;
+    }
 
-    if(!caps)
-    {
-        if(getenv(wrapperDebug))
-            fprintf(stderr, "no caps set or could not retrieve the caps for this file, not doing anything...");
+    size_t size;
+    uint32_t version = LE32_TO_H(data.magic_etc) & VFS_CAP_REVISION_MASK;
+    switch (version) {
+        case VFS_CAP_REVISION_1:
+            size = VFS_CAP_U32_1;
+            break;
+        case VFS_CAP_REVISION_2:
+        case VFS_CAP_REVISION_3:
+            size = VFS_CAP_U32_3;
+            break;
+        default:
+            fprintf(stderr, "BUG! Unsupported capability version 0x%x on %s. Report to NixOS bugtracker\n", version, self_path);
+            return 1;
+    }
 
-        return 1;
+    const struct __user_cap_header_struct header = {
+      .version = _LINUX_CAPABILITY_VERSION_3,
+      .pid = getpid(),
+    };
+    struct __user_cap_data_struct user_data[2] = {};
+
+    for (size_t i = 0; i < size; i++) {
+        // merge inheritable & permitted into one
+        user_data[i].permitted = user_data[i].inheritable =
+            LE32_TO_H(data.data[i].inheritable) | LE32_TO_H(data.data[i].permitted);
     }
 
-    // We use `cap_to_text` and iteration over the tokenized result
-    // string because, as of libcap's current release, there is no
-    // facility for retrieving an array of `cap_value_t`'s that can be
-    // given to `prctl` in order to lift that capability into the
-    // Ambient set.
-    //
-    // Some discussion was had around shot-gunning all of the
-    // capabilities we know about into the Ambient set but that has a
-    // security smell and I deemed the risk of the current
-    // implementation crashing the program to be lower than the risk
-    // of a privilege escalation security hole being introduced by
-    // raising all capabilities, even ones we didn't intend for the
-    // program, into the Ambient set.
-    //
-    // `cap_t` which is returned by `cap_get_*` is an opaque type and
-    // even if we could retrieve the bitmasks (which, as far as I can
-    // tell we cannot) in order to get the `cap_value_t`
-    // representation for each capability we would have to take the
-    // total number of capabilities supported and iterate over the
-    // sequence of integers up-to that maximum total, testing each one
-    // against the bitmask ((bitmask >> n) & 1) to see if it's set and
-    // aggregating each "capability integer n" that is set in the
-    // bitmask.
-    //
-    // That, combined with the fact that we can't easily get the
-    // bitmask anyway seemed much more brittle than fetching the
-    // `cap_t`, transforming it into a textual representation,
-    // tokenizing the string, and using `cap_from_name` on the token
-    // to get the `cap_value_t` that we need for `prctl`. There is
-    // indeed risk involved if the output string format of
-    // `cap_to_text` ever changes but at this time the combination of
-    // factors involving the below list have led me to the conclusion
-    // that the best implementation at this time is reading then
-    // parsing with *lots of documentation* about why we're doing it
-    // this way.
-    //
-    // 1. No explicit API for fetching an array of `cap_value_t`'s or
-    //    for transforming a `cap_t` into such a representation
-    // 2. The risk of a crash is lower than lifting all capabilities
-    //    into the Ambient set
-    // 3. libcap is depended on heavily in the Linux ecosystem so
-    //    there is a high chance that the output representation of
-    //    `cap_to_text` will not change which reduces our risk that
-    //    this parsing step will cause a crash
-    //
-    // The preferred method, should it ever be available in the
-    // future, would be to use libcap API's to transform the result
-    // from a `cap_get_*` into an array of `cap_value_t`'s that can
-    // then be given to prctl.
-    //
-    // - Parnell
-    ssize_t capLen;
-    char* capstr = cap_to_text(caps, &capLen);
-    cap_free(caps);
-    
-    // TODO: For now, we assume that cap_to_text always starts its
-    // result string with " =" and that the first capability is listed
-    // immediately after that. We should verify this.
-    assert(capLen >= 2);
-    capstr += 2;
-
-    char* saveptr = NULL;
-    for(char* tok = strtok_r(capstr, ",", &saveptr); tok; tok = strtok_r(NULL, ",", &saveptr))
-    {
-      cap_value_t capnum;
-      if (cap_from_name(tok, &capnum))
-      {
-          if(getenv(wrapperDebug))
-              fprintf(stderr, "cap_from_name failed, skipping: %s", tok);
-      }
-      else if (capnum == CAP_SETPCAP)
-      {
-          // Check for the cap_setpcap capability, we set this on the
-          // wrapper so it can elevate the capabilities to the Ambient
-          // set but we do not want to propagate it down into the
-          // wrapped program.
-          //
-          // TODO: what happens if that's the behavior you want
-          // though???? I'm preferring a strict vs. loose policy here.
-          if(getenv(wrapperDebug))
-              fprintf(stderr, "cap_setpcap in set, skipping it\n");
-      }
-      else
-      {
-          set_ambient_cap(capnum);
-
-          if(getenv(wrapperDebug))
-              fprintf(stderr, "raised %s into the Ambient capability set\n", tok);
-      }
+    if (syscall(SYS_capset, &header, &user_data) < 0) {
+        fprintf(stderr, "failed to inherit capabilities: %s", strerror(errno));
+        return 1;
+    }
+    unsigned last_cap;
+    r = get_last_cap(&last_cap);
+    if (r < 0) {
+        return 1;
+    }
+    uint64_t set = user_data[0].permitted | (uint64_t)user_data[1].permitted << 32;
+    for (unsigned cap = 0; cap < last_cap; cap++) {
+        if (!(set & (1ULL << cap))) {
+            continue;
+        }
+
+        // Check for the cap_setpcap capability, we set this on the
+        // wrapper so it can elevate the capabilities to the Ambient
+        // set but we do not want to propagate it down into the
+        // wrapped program.
+        //
+        // TODO: what happens if that's the behavior you want
+        // though???? I'm preferring a strict vs. loose policy here.
+        if (cap == CAP_SETPCAP) {
+            if(getenv(wrapper_debug)) {
+                fprintf(stderr, "cap_setpcap in set, skipping it\n");
+            }
+            continue;
+        }
+        if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0)) {
+            fprintf(stderr, "cannot raise the capability %d into the ambient set: %s\n", cap, strerror(errno));
+            return 1;
+        }
+        if (getenv(wrapper_debug)) {
+            fprintf(stderr, "raised %d into the ambient capability set\n", cap);
+        }
     }
-    cap_free(capstr);
 
     return 0;
 }
 
-int main(int argc, char * * argv)
-{
-    // I *think* it's safe to assume that a path from a symbolic link
-    // should safely fit within the PATH_MAX system limit. Though I'm
-    // not positive it's safe...
-    char selfPath[PATH_MAX];
-    int selfPathSize = readlink("/proc/self/exe", selfPath, sizeof(selfPath));
-
-    assert(selfPathSize > 0);
-
-    // Assert we have room for the zero byte, this ensures the path
-    // isn't being truncated because it's too big for the buffer.
-    //
-    // A better way to handle this might be to use something like the
-    // whereami library (https://github.com/gpakosz/whereami) or a
-    // loop that resizes the buffer and re-reads the link if the
-    // contents are being truncated.
-    assert(selfPathSize < sizeof(selfPath));
+int readlink_malloc(const char *p, char **ret) {
+    size_t l = FILENAME_MAX+1;
+    int r;
+
+    for (;;) {
+        char *c = calloc(l, sizeof(char));
+        if (!c) {
+            return -ENOMEM;
+        }
+
+        ssize_t n = readlink(p, c, l-1);
+        if (n < 0) {
+            r = -errno;
+            free(c);
+            return r;
+        }
+
+        if ((size_t) n < l-1) {
+            c[n] = 0;
+            *ret = c;
+            return 0;
+        }
+
+        free(c);
+        l *= 2;
+    }
+}
 
-    // Set the zero byte since readlink doesn't do that for us.
-    selfPath[selfPathSize] = '\0';
+int main(int argc, char **argv) {
+    char *self_path = NULL;
+    int self_path_size = readlink_malloc("/proc/self/exe", &self_path);
+    if (self_path_size < 0) {
+        fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
+    }
 
     // Make sure that we are being executed from the right location,
-    // i.e., `safeWrapperDir'.  This is to prevent someone from creating
+    // i.e., `safe_wrapper_dir'.  This is to prevent someone from creating
     // hard link `X' from some other location, along with a false
     // `X.real' file, to allow arbitrary programs from being executed
     // with elevated capabilities.
-    int len = strlen(wrapperDir);
-    if (len > 0 && '/' == wrapperDir[len - 1])
+    int len = strlen(wrapper_dir);
+    if (len > 0 && '/' == wrapper_dir[len - 1])
       --len;
-    assert(!strncmp(selfPath, wrapperDir, len));
-    assert('/' == wrapperDir[0]);
-    assert('/' == selfPath[len]);
+    assert(!strncmp(self_path, wrapper_dir, len));
+    assert('/' == wrapper_dir[0]);
+    assert('/' == self_path[len]);
 
     // Make *really* *really* sure that we were executed as
-    // `selfPath', and not, say, as some other setuid program. That
+    // `self_path', and not, say, as some other setuid program. That
     // is, our effective uid/gid should match the uid/gid of
-    // `selfPath'.
+    // `self_path'.
     struct stat st;
-    assert(lstat(selfPath, &st) != -1);
+    assert(lstat(self_path, &st) != -1);
 
     assert(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
     assert(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
@@ -207,33 +199,35 @@ int main(int argc, char * * argv)
     assert(!(st.st_mode & (S_IWGRP | S_IWOTH)));
 
     // Read the path of the real (wrapped) program from <self>.real.
-    char realFN[PATH_MAX + 10];
-    int realFNSize = snprintf (realFN, sizeof(realFN), "%s.real", selfPath);
-    assert (realFNSize < sizeof(realFN));
+    char real_fn[PATH_MAX + 10];
+    int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path);
+    assert(real_fn_size < sizeof(real_fn));
 
-    int fdSelf = open(realFN, O_RDONLY);
-    assert (fdSelf != -1);
+    int fd_self = open(real_fn, O_RDONLY);
+    assert(fd_self != -1);
 
-    char sourceProg[PATH_MAX];
-    len = read(fdSelf, sourceProg, PATH_MAX);
-    assert (len != -1);
-    assert (len < sizeof(sourceProg));
-    assert (len > 0);
-    sourceProg[len] = 0;
+    char source_prog[PATH_MAX];
+    len = read(fd_self, source_prog, PATH_MAX);
+    assert(len != -1);
+    assert(len < sizeof(source_prog));
+    assert(len > 0);
+    source_prog[len] = 0;
 
-    close(fdSelf);
+    close(fd_self);
 
     // Read the capabilities set on the wrapper and raise them in to
-    // the Ambient set so the program we're wrapping receives the
+    // the ambient set so the program we're wrapping receives the
     // capabilities too!
-    make_caps_ambient(selfPath);
+    if (make_caps_ambient(self_path) != 0) {
+        free(self_path);
+        return 1;
+    }
+    free(self_path);
 
-    execve(sourceProg, argv, environ);
+    execve(source_prog, argv, environ);
     
     fprintf(stderr, "%s: cannot run `%s': %s\n",
-        argv[0], sourceProg, strerror(errno));
+        argv[0], source_prog, strerror(errno));
 
-    exit(1);
+    return 1;
 }
-
-
diff --git a/nixos/modules/security/wrappers/wrapper.nix b/nixos/modules/security/wrappers/wrapper.nix
new file mode 100644
index 0000000000000..e3620fb222d2c
--- /dev/null
+++ b/nixos/modules/security/wrappers/wrapper.nix
@@ -0,0 +1,21 @@
+{ stdenv, linuxHeaders, parentWrapperDir, debug ? false }:
+# For testing:
+# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }'
+stdenv.mkDerivation {
+  name = "security-wrapper";
+  buildInputs = [ linuxHeaders ];
+  dontUnpack = true;
+  hardeningEnable = [ "pie" ];
+  CFLAGS = [
+    ''-DWRAPPER_DIR="${parentWrapperDir}"''
+  ] ++ (if debug then [
+    "-Werror" "-Og" "-g"
+  ] else [
+    "-Wall" "-O2"
+  ]);
+  dontStrip = debug;
+  installPhase = ''
+    mkdir -p $out/bin
+    $CC $CFLAGS ${./wrapper.c} -o $out/bin/security-wrapper
+  '';
+}
diff --git a/nixos/modules/services/admin/salt/master.nix b/nixos/modules/services/admin/salt/master.nix
index c6b1b0cc0bd8e..a3069c81c19ae 100644
--- a/nixos/modules/services/admin/salt/master.nix
+++ b/nixos/modules/services/admin/salt/master.nix
@@ -45,7 +45,7 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       path = with pkgs; [
-        utillinux  # for dmesg
+        util-linux  # for dmesg
       ];
       serviceConfig = {
         ExecStart = "${pkgs.salt}/bin/salt-master";
@@ -59,5 +59,5 @@ in
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ aneeshusa ];
+  meta.maintainers = with lib.maintainers; [ Flakebi ];
 }
diff --git a/nixos/modules/services/admin/salt/minion.nix b/nixos/modules/services/admin/salt/minion.nix
index c8fa9461a2094..ac124c570d8d4 100644
--- a/nixos/modules/services/admin/salt/minion.nix
+++ b/nixos/modules/services/admin/salt/minion.nix
@@ -50,7 +50,7 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       path = with pkgs; [
-        utillinux
+        util-linux
       ];
       serviceConfig = {
         ExecStart = "${pkgs.salt}/bin/salt-minion";
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
index 160dbddcd487d..178b2f6e144bd 100644
--- a/nixos/modules/services/amqp/activemq/default.nix
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -33,6 +33,7 @@ in {
       };
       configurationDir = mkOption {
         default = "${activemq}/conf";
+        type = types.str;
         description = ''
           The base directory for ActiveMQ's configuration.
           By default, this directory is searched for a file named activemq.xml,
diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix
index 3fe76a1654010..aff3fe2ba42d4 100644
--- a/nixos/modules/services/audio/alsa.nix
+++ b/nixos/modules/services/audio/alsa.nix
@@ -32,7 +32,7 @@ in
 
       enableOSSEmulation = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!).
         '';
diff --git a/nixos/modules/services/audio/jack.nix b/nixos/modules/services/audio/jack.nix
index ceff366d0bbb2..bee97dbfc6b3d 100644
--- a/nixos/modules/services/audio/jack.nix
+++ b/nixos/modules/services/audio/jack.nix
@@ -246,6 +246,9 @@ in {
         description = "JACK Audio Connection Kit";
         serviceConfig = {
           User = "jackaudio";
+          SupplementaryGroups = lib.optional
+            (config.hardware.pulseaudio.enable
+            && !config.hardware.pulseaudio.systemWide) "users";
           ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
           LimitRTPRIO = 99;
           LimitMEMLOCK = "infinity";
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index ba20b1b98d971..9f01e29dd0e98 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -10,6 +10,14 @@ let
   gid = config.ids.gids.mpd;
   cfg = config.services.mpd;
 
+  credentialsPlaceholder = (creds:
+    let
+      placeholders = (imap0
+        (i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
+        creds);
+    in
+      concatStringsSep "\n" placeholders);
+
   mpdConf = pkgs.writeText "mpd.conf" ''
     # This file was automatically generated by NixOS. Edit mpd's configuration
     # via NixOS' configuration.nix, as this file will be rewritten upon mpd's
@@ -32,6 +40,8 @@ let
       }
     ''}
 
+    ${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
+
     ${cfg.extraConfig}
   '';
 
@@ -64,18 +74,24 @@ in {
       musicDirectory = mkOption {
         type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
         default = "${cfg.dataDir}/music";
-        defaultText = ''''${dataDir}/music'';
+        defaultText = "\${dataDir}/music";
         description = ''
-          The directory or NFS/SMB network share where mpd reads music from.
+          The directory or NFS/SMB network share where MPD reads music from. If left
+          as the default value this directory will automatically be created before
+          the MPD server starts, otherwise the sysadmin is responsible for ensuring
+          the directory exists with appropriate ownership and permissions.
         '';
       };
 
       playlistDirectory = mkOption {
         type = types.path;
         default = "${cfg.dataDir}/playlists";
-        defaultText = ''''${dataDir}/playlists'';
+        defaultText = "\${dataDir}/playlists";
         description = ''
-          The directory where mpd stores playlists.
+          The directory where MPD stores playlists. If left as the default value
+          this directory will automatically be created before the MPD server starts,
+          otherwise the sysadmin is responsible for ensuring the directory exists
+          with appropriate ownership and permissions.
         '';
       };
 
@@ -94,8 +110,10 @@ in {
         type = types.path;
         default = "/var/lib/${name}";
         description = ''
-          The directory where MPD stores its state, tag cache,
-          playlists etc.
+          The directory where MPD stores its state, tag cache, playlists etc. If
+          left as the default value this directory will automatically be created
+          before the MPD server starts, otherwise the sysadmin is responsible for
+          ensuring the directory exists with appropriate ownership and permissions.
         '';
       };
 
@@ -137,23 +155,42 @@ in {
       dbFile = mkOption {
         type = types.nullOr types.str;
         default = "${cfg.dataDir}/tag_cache";
-        defaultText = ''''${dataDir}/tag_cache'';
+        defaultText = "\${dataDir}/tag_cache";
         description = ''
           The path to MPD's database. If set to <literal>null</literal> the
           parameter is omitted from the configuration.
         '';
       };
 
-      credentialsFile = mkOption {
-        type = types.path;
+      credentials = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              description = ''
+                Path to file containing the password.
+              '';
+            };
+            permissions = let
+              perms = ["read" "add" "control" "admin"];
+            in mkOption {
+              type = types.listOf (types.enum perms);
+              default = [ "read" ];
+              description = ''
+                List of permissions that are granted with this password.
+                Permissions can be "${concatStringsSep "\", \"" perms}".
+              '';
+            };
+          };
+        });
         description = ''
-          Path to a file to be merged with the settings during the service startup.
-          Useful to merge a file which is better kept out of the Nix store
-          because it contains sensible data like MPD's password. Example may look like this:
-          <literal>password "myMpdPassword@read,add,control,admin"</literal>
+          Credentials and permissions for accessing the mpd server.
         '';
-        default = "/dev/null";
-        example = "/var/lib/secrets/mpd.conf";
+        default = [];
+        example = [
+          {passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
+          {passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
+        ];
       };
 
       fluidsynth = mkOption {
@@ -185,44 +222,46 @@ in {
       };
     };
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.playlistDirectory}' - ${cfg.user} ${cfg.group} - -"
-    ];
-
     systemd.services.mpd = {
       after = [ "network.target" "sound.target" ];
       description = "Music Player Daemon";
       wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
 
-      serviceConfig = {
-        User = "${cfg.user}";
-        ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon /etc/mpd.conf";
-        ExecStartPre = pkgs.writeScript "mpd-start-pre" ''
-          #!${pkgs.runtimeShell}
-          set -euo pipefail
-          cat ${mpdConf} ${cfg.credentialsFile} > /etc/mpd.conf
-        '';
-        Type = "notify";
-        LimitRTPRIO = 50;
-        LimitRTTIME = "infinity";
-        ProtectSystem = true;
-        NoNewPrivileges = true;
-        ProtectKernelTunables = true;
-        ProtectControlGroups = true;
-        ProtectKernelModules = true;
-        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
-        RestrictNamespaces = true;
-        Restart = "always";
-      };
-    };
-    environment.etc."mpd.conf" = {
-      mode = "0640";
-      group = cfg.group;
-      user = cfg.user;
-      # To be modified by the service' ExecStartPre
-      text = ''
-      '';
+      serviceConfig = mkMerge [
+        {
+          User = "${cfg.user}";
+          ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon /run/mpd/mpd.conf";
+          ExecStartPre = pkgs.writeShellScript "mpd-start-pre" ''
+            set -euo pipefail
+            install -m 600 ${mpdConf} /run/mpd/mpd.conf
+            ${optionalString (cfg.credentials != [])
+            "${pkgs.replace}/bin/replace-literal -fe ${
+              concatStringsSep " -a " (imap0 (i: c: "\"{{password-${toString i}}}\" \"$(cat ${c.passwordFile})\"") cfg.credentials)
+            } /run/mpd/mpd.conf"}
+          '';
+          RuntimeDirectory = "mpd";
+          Type = "notify";
+          LimitRTPRIO = 50;
+          LimitRTTIME = "infinity";
+          ProtectSystem = true;
+          NoNewPrivileges = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectKernelModules = true;
+          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
+          RestrictNamespaces = true;
+          Restart = "always";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/${name}") {
+          StateDirectory = [ name ];
+        })
+        (mkIf (cfg.playlistDirectory == "/var/lib/${name}/playlists") {
+          StateDirectory = [ name "${name}/playlists" ];
+        })
+        (mkIf (cfg.musicDirectory == "/var/lib/${name}/music") {
+          StateDirectory = [ name "${name}/music" ];
+        })
+      ];
     };
 
     users.users = optionalAttrs (cfg.user == name) {
diff --git a/nixos/modules/services/audio/mpdscribble.nix b/nixos/modules/services/audio/mpdscribble.nix
new file mode 100644
index 0000000000000..642d8743935f1
--- /dev/null
+++ b/nixos/modules/services/audio/mpdscribble.nix
@@ -0,0 +1,202 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mpdscribble;
+  mpdCfg = config.services.mpd;
+
+  endpointUrls = {
+    "last.fm" = "http://post.audioscrobbler.com";
+    "libre.fm" = "http://turtle.libre.fm";
+    "jamendo" = "http://postaudioscrobbler.jamendo.com";
+    "listenbrainz" = "http://proxy.listenbrainz.org";
+  };
+
+  mkSection = secname: secCfg: ''
+    [${secname}]
+    url      = ${secCfg.url}
+    username = ${secCfg.username}
+    password = {{${secname}_PASSWORD}}
+    journal  = /var/lib/mpdscribble/${secname}.journal
+  '';
+
+  endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
+  cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
+    ## This file was automatically genenrated by NixOS and will be overwritten.
+    ## Do not edit. Edit your NixOS configuration instead.
+
+    ## mpdscribble - an audioscrobbler for the Music Player Daemon.
+    ## http://mpd.wikia.com/wiki/Client:mpdscribble
+
+    # HTTP proxy URL.
+    ${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
+
+    # The location of the mpdscribble log file.  The special value
+    # "syslog" makes mpdscribble use the local syslog daemon.  On most
+    # systems, log messages will appear in /var/log/daemon.log then.
+    # "-" means log to stderr (the current terminal).
+    log = -
+
+    # How verbose mpdscribble's logging should be.  Default is 1.
+    verbose = ${toString cfg.verbose}
+
+    # How often should mpdscribble save the journal file? [seconds]
+    journal_interval = ${toString cfg.journalInterval}
+
+    # The host running MPD, possibly protected by a password
+    # ([PASSWORD@]HOSTNAME).
+    host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
+
+    # The port that the MPD listens on and mpdscribble should try to
+    # connect to.
+    port = ${toString cfg.port}
+
+    ${endpoints}
+  '';
+
+  cfgFile = "/run/mpdscribble/mpdscribble.conf";
+
+  replaceSecret = secretFile: placeholder: targetFile:
+    optionalString (secretFile != null) ''
+      ${pkgs.replace}/bin/replace-literal -ef ${placeholder} "$(cat ${secretFile})" ${targetFile}'';
+
+  preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
+    cp -f "${cfgTemplate}" "${cfgFile}"
+    ${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
+    ${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
+      replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
+      cfg.endpoints)}
+  '';
+
+  localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
+
+in {
+  ###### interface
+
+  options.services.mpdscribble = {
+
+    enable = mkEnableOption "mpdscribble";
+
+    proxy = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = ''
+        HTTP proxy URL.
+      '';
+    };
+
+    verbose = mkOption {
+      default = 1;
+      type = types.int;
+      description = ''
+        Log level for the mpdscribble daemon.
+      '';
+    };
+
+    journalInterval = mkOption {
+      default = 600;
+      example = 60;
+      type = types.int;
+      description = ''
+        How often should mpdscribble save the journal file? [seconds]
+      '';
+    };
+
+    host = mkOption {
+      default = (if mpdCfg.network.listenAddress != "any" then
+        mpdCfg.network.listenAddress
+      else
+        "localhost");
+      type = types.str;
+      description = ''
+        Host for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    passwordFile = mkOption {
+      default = if localMpd then
+        (findFirst
+          (c: any (x: x == "read") c.permissions)
+          { passwordFile = null; }
+          mpdCfg.credentials).passwordFile
+      else
+        null;
+      type = types.nullOr types.str;
+      description = ''
+        File containing the password for the mpd daemon.
+        If there is a local mpd configured using <option>services.mpd.credentials</option>
+        the default is automatically set to a matching passwordFile of the local mpd.
+      '';
+    };
+
+    port = mkOption {
+      default = mpdCfg.network.port;
+      type = types.port;
+      description = ''
+        Port for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    endpoints = mkOption {
+      type = (let
+        endpoint = { name, ... }: {
+          options = {
+            url = mkOption {
+              type = types.str;
+              default = endpointUrls.${name} or "";
+              description =
+                "The url endpoint where the scrobble API is listening.";
+            };
+            username = mkOption {
+              type = types.str;
+              description = ''
+                Username for the scrobble service.
+              '';
+            };
+            passwordFile = mkOption {
+              type = types.nullOr types.str;
+              description =
+                "File containing the password, either as MD5SUM or cleartext.";
+            };
+          };
+        };
+      in types.attrsOf (types.submodule endpoint));
+      default = { };
+      example = {
+        "last.fm" = {
+          username = "foo";
+          passwordFile = "/run/secrets/lastfm_password";
+        };
+      };
+      description = ''
+        Endpoints to scrobble to.
+        If the endpoint is one of "${
+          concatStringsSep "\", \"" (attrNames endpointUrls)
+        }" the url is set automatically.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.mpdscribble = {
+      after = [ "network.target" ] ++ (optional localMpd "mpd.service");
+      description = "mpdscribble mpd scrobble client";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "mpdscribble";
+        RuntimeDirectory = "mpdscribble";
+        RuntimeDirectoryMode = "700";
+        # TODO use LoadCredential= instead of running preStart with full privileges?
+        ExecStartPre = "+${preStart}";
+        ExecStart =
+          "${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index f614f0ba3e10c..a261b87607805 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -48,8 +48,8 @@ let
     ++ [ "--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}"
+    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${toString cfg.streamBuffer}"
+    ++ optionalNull cfg.buffer "--stream.buffer ${toString cfg.buffer}"
     ++ optional cfg.sendToMuted "--stream.send_to_muted"
     # tcp json rpc
     ++ [ "--tcp.enabled ${toString cfg.tcp.enable}" ]
@@ -198,13 +198,14 @@ in {
         type = with types; attrsOf (submodule {
           options = {
             location = mkOption {
-              type = types.path;
+              type = types.oneOf [ types.path types.str ];
               description = ''
-                The location of the pipe.
+                The location of the pipe, file, Librespot/Airplay/process binary, or a TCP address.
+                Use an empty string for alsa.
               '';
             };
             type = mkOption {
-              type = types.enum [ "pipe" "file" "process" "spotify" "airplay" ];
+              type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" ];
               default = "pipe";
               description = ''
                 The type of input stream.
@@ -219,13 +220,21 @@ in {
               example = literalExample ''
                 # for type == "pipe":
                 {
-                  mode = "listen";
+                  mode = "create";
                 };
                 # for type == "process":
                 {
                   params = "--param1 --param2";
                   logStderr = "true";
                 };
+                # for type == "tcp":
+                {
+                  mode = "client";
+                }
+                # for type == "alsa":
+                {
+                  device = "hw:0,0";
+                }
               '';
             };
             inherit sampleFormat;
@@ -255,6 +264,11 @@ in {
 
   config = mkIf cfg.enable {
 
+    # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
+    warnings = filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
+      services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
+    '' else "") cfg.streams);
+
     systemd.services.snapserver = {
       after = [ "network.target" ];
       description = "Snapserver";
@@ -272,7 +286,7 @@ in {
         ProtectKernelTunables = true;
         ProtectControlGroups = true;
         ProtectKernelModules = true;
-        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
         RestrictNamespaces = true;
         RuntimeDirectory = name;
         StateDirectory = name;
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index 3d69a69038a3c..cc8b77cbfbe8e 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -1,5 +1,6 @@
 { config, lib, pkgs, ... }:
 
+
 # TODO: test configuration when building nixexpr (use -t parameter)
 # TODO: support sqlite3 (it's deprecate?) and mysql
 
@@ -111,6 +112,7 @@ let
   {
     options = {
       password = mkOption {
+        type = types.str;
         # TODO: required?
         description = ''
           Specifies the password that must be supplied for the default Bacula
@@ -130,6 +132,7 @@ let
       };
 
       monitor = mkOption {
+        type = types.enum [ "no" "yes" ];
         default = "no";
         example = "yes";
         description = ''
@@ -150,6 +153,7 @@ let
   {
     options = {
       changerDevice = mkOption {
+        type = types.str;
         description = ''
           The specified name-string must be the generic SCSI device name of the
           autochanger that corresponds to the normal read/write Archive Device
@@ -168,6 +172,7 @@ let
       };
 
       changerCommand = mkOption {
+        type = types.str;
         description = ''
           The name-string specifies an external program to be called that will
           automatically change volumes as required by Bacula. Normally, this
@@ -190,12 +195,13 @@ let
       };
 
       devices = mkOption {
-        description = ''
-        '';
+        description = "";
+        type = types.listOf types.str;
       };
 
       extraAutochangerConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Autochanger directive.
         '';
@@ -212,6 +218,7 @@ let
     options = {
       archiveDevice = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
           The specified name-string gives the system file name of the storage
           device managed by this storage daemon. This will usually be the
@@ -228,6 +235,7 @@ let
 
       mediaType = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
           The specified name-string names the type of media supported by this
           device, for example, <literal>DLT7000</literal>. Media type names are
@@ -265,6 +273,7 @@ let
 
       extraDeviceConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Device directive.
         '';
@@ -293,6 +302,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-fd";
+        type = types.str;
         description = ''
           The client name that must be used by the Director when connecting.
           Generally, it is a good idea to use a name related to the machine so
@@ -321,6 +331,7 @@ in {
 
       extraClientConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Client directive.
         '';
@@ -332,6 +343,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -352,6 +364,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-sd";
+        type = types.str;
         description = ''
           Specifies the Name of the Storage daemon.
         '';
@@ -392,6 +405,7 @@ in {
 
       extraStorageConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Storage directive.
         '';
@@ -403,6 +417,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -424,6 +439,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-dir";
+        type = types.str;
         description = ''
           The director name used by the system administrator. This directive is
           required.
@@ -445,6 +461,7 @@ in {
 
       password = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
            Specifies the password that must be supplied for a Director.
         '';
@@ -452,6 +469,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -462,6 +480,7 @@ in {
 
       extraDirectorConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Director directive.
         '';
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 31d606b141a8d..506ded5e9e8c4 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -48,6 +48,7 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = defaultUser;
         description = ''
           User to be used to perform backup.
@@ -56,12 +57,14 @@ in
 
       databases = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of database names to dump.
         '';
       };
 
       location = mkOption {
+        type = types.path;
         default = "/var/backup/mysql";
         description = ''
           Location to put the gzipped MySQL database dumps.
@@ -70,6 +73,7 @@ in
 
       singleTransaction = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Whether to create database dump in a single transaction
         '';
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
index 428861a7598a1..f4bd3aa447e5e 100644
--- a/nixos/modules/services/backup/postgresql-backup.nix
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -48,6 +48,7 @@ in {
 
       startAt = mkOption {
         default = "*-*-* 01:15:00";
+        type = types.str;
         description = ''
           This option defines (see <literal>systemd.time</literal> for format) when the
           databases should be dumped.
@@ -70,6 +71,7 @@ in {
 
       databases = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of database names to dump.
         '';
@@ -77,6 +79,7 @@ in {
 
       location = mkOption {
         default = "/var/backup/postgresql";
+        type = types.path;
         description = ''
           Location to put the gzipped PostgreSQL database dumps.
         '';
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index d869835bf07e6..573f0efa9da41 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -243,9 +243,11 @@ in
           restartIfChanged = false;
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = [ "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
+            ExecStart = [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
             User = backup.user;
             RuntimeDirectory = "restic-backups-${name}";
+            CacheDirectory = "restic-backups-${name}";
+            CacheDirectoryMode = "0700";
           } // optionalAttrs (backup.s3CredentialsFile != null) {
             EnvironmentFile = backup.s3CredentialsFile;
           };
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index fff119c2cf009..e72e3fa59cf92 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -4,6 +4,15 @@ with lib;
 
 let
   cfg = config.services.syncoid;
+
+  # Extract pool names of local datasets (ones that don't contain "@") that
+  # have the specified type (either "source" or "target")
+  getPools = type: unique (map (d: head (builtins.match "([^/]+).*" d)) (
+    # Filter local datasets
+    filter (d: !hasInfix "@" d)
+    # Get datasets of the specified type
+    (catAttrs type (attrValues cfg.commands))
+  ));
 in {
 
     # Interface
@@ -26,14 +35,25 @@ in {
 
       user = mkOption {
         type = types.str;
-        default = "root";
+        default = "syncoid";
         example = "backup";
         description = ''
-          The user for the service. Sudo or ZFS privilege delegation must be
-          configured to use a user other than root.
+          The user for the service. ZFS privilege delegation will be
+          automatically configured for any local pools used by syncoid if this
+          option is set to a user other than root. The user will be given the
+          "hold" and "send" privileges on any pool that has datasets being sent
+          and the "create", "mount", "receive", and "rollback" privileges on
+          any pool that has datasets being received.
         '';
       };
 
+      group = mkOption {
+        type = types.str;
+        default = "syncoid";
+        example = "backup";
+        description = "The group for the service.";
+      };
+
       sshKey = mkOption {
         type = types.nullOr types.path;
         # Prevent key from being copied to store
@@ -150,6 +170,18 @@ in {
     # Implementation
 
     config = mkIf cfg.enable {
+      users =  {
+        users = mkIf (cfg.user == "syncoid") {
+          syncoid = {
+            group = cfg.group;
+            isSystemUser = true;
+          };
+        };
+        groups = mkIf (cfg.group == "syncoid") {
+          syncoid = {};
+        };
+      };
+
       systemd.services.syncoid = {
         description = "Syncoid ZFS synchronization service";
         script = concatMapStringsSep "\n" (c: lib.escapeShellArgs
@@ -160,10 +192,22 @@ in {
             ++ c.extraArgs
             ++ [ "--sendoptions" c.sendOptions
                  "--recvoptions" c.recvOptions
+                 "--no-privilege-elevation"
                  c.source c.target
                ])) (attrValues cfg.commands);
         after = [ "zfs.target" ];
-        serviceConfig.User = cfg.user;
+        serviceConfig = {
+          ExecStartPre = (map (pool: lib.escapeShellArgs [
+            "+/run/booted-system/sw/bin/zfs" "allow"
+            cfg.user "hold,send" pool
+          ]) (getPools "source")) ++
+          (map (pool: lib.escapeShellArgs [
+            "+/run/booted-system/sw/bin/zfs" "allow"
+            cfg.user "create,mount,receive,rollback" pool
+          ]) (getPools "target"));
+          User = cfg.user;
+          Group = cfg.group;
+        };
         startAt = cfg.interval;
       };
     };
diff --git a/nixos/modules/services/backup/tarsnap.nix b/nixos/modules/services/backup/tarsnap.nix
index 6d99a1efb6138..8187042b4b801 100644
--- a/nixos/modules/services/backup/tarsnap.nix
+++ b/nixos/modules/services/backup/tarsnap.nix
@@ -29,13 +29,7 @@ in
 
   options = {
     services.tarsnap = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable periodic tarsnap backups.
-        '';
-      };
+      enable = mkEnableOption "periodic tarsnap backups";
 
       keyfile = mkOption {
         type = types.str;
@@ -279,7 +273,8 @@ in
           Tarsnap archive configurations. Each attribute names an archive
           to be created at a given time interval, according to the options
           associated with it. When uploading to the tarsnap server,
-          archive names are suffixed by a 1 second resolution timestamp.
+          archive names are suffixed by a 1 second resolution timestamp,
+          with the format <literal>%Y%m%d%H%M%S</literal>.
 
           For each member of the set is created a timer which triggers the
           instanced <literal>tarsnap-archive-name</literal> service unit. You may use
@@ -308,7 +303,7 @@ in
         requires    = [ "network-online.target" ];
         after       = [ "network-online.target" ];
 
-        path = with pkgs; [ iputils tarsnap utillinux ];
+        path = with pkgs; [ iputils tarsnap util-linux ];
 
         # In order for the persistent tarsnap timer to work reliably, we have to
         # make sure that the tarsnap server is reachable after systemd starts up
@@ -355,11 +350,11 @@ in
         description = "Tarsnap restore '${name}'";
         requires    = [ "network-online.target" ];
 
-        path = with pkgs; [ iputils tarsnap utillinux ];
+        path = with pkgs; [ iputils tarsnap util-linux ];
 
         script = let
           tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
-          lastArchive = ''$(${tarsnap} --list-archives | sort | tail -1)'';
+          lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
           run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}'';
 
         in if (cfg.cachedir != null) then ''
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
index bfb73f6837159..41ac46e538e35 100644
--- a/nixos/modules/services/cluster/hadoop/default.nix
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -7,6 +7,7 @@ with lib;
   options.services.hadoop = {
     coreSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "fs.defaultFS" = "hdfs://localhost";
@@ -17,6 +18,7 @@ with lib;
 
     hdfsSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "dfs.nameservices" = "namenode1";
@@ -27,6 +29,7 @@ with lib;
 
     mapredSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "mapreduce.map.cpu.vcores" = "1";
@@ -37,6 +40,7 @@ with lib;
 
     yarnSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "yarn.resourcemanager.ha.id" = "resourcemanager1";
@@ -50,8 +54,7 @@ with lib;
       default = pkgs.hadoop;
       defaultText = "pkgs.hadoop";
       example = literalExample "pkgs.hadoop";
-      description = ''
-      '';
+      description = "";
     };
   };
 
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 2e8bf20a68fc4..e62fbc94415ca 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -47,6 +47,7 @@ in
 
     extraFlags = mkOption {
       description = "Extra flags to pass to the k3s command.";
+      type = types.str;
       default = "";
       example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
     };
@@ -76,6 +77,10 @@ in
       enable = mkDefault true;
     };
 
+    # TODO: disable this once k3s supports cgroupsv2, either by docker
+    # supporting it, or their bundled containerd
+    systemd.enableUnifiedCgroupHierarchy = false;
+
     systemd.services.k3s = {
       description = "k3s service";
       after = mkIf cfg.docker [ "docker.service" ];
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index c3d67552cc8c3..479027f1b2708 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -241,7 +241,17 @@ in
         description = "Kubernetes Kubelet Service";
         wantedBy = [ "kubernetes.target" ];
         after = [ "network.target" "docker.service" "kube-apiserver.service" ];
-        path = with pkgs; [ gitMinimal openssh docker utillinux iproute ethtool thin-provisioning-tools iptables socat ] ++ top.path;
+        path = with pkgs; [
+          gitMinimal
+          openssh
+          docker
+          util-linux
+          iproute
+          ethtool
+          thin-provisioning-tools
+          iptables
+          socat
+        ] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package ++ top.path;
         preStart = ''
           ${concatMapStrings (img: ''
             echo "Seeding docker image: ${img}"
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 705390a21d4ee..7363441e5387a 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -14,8 +14,8 @@ let
       ClusterName=${cfg.clusterName}
       StateSaveLocation=${cfg.stateSaveLocation}
       SlurmUser=${cfg.user}
-      ${optionalString (cfg.controlMachine != null) ''controlMachine=${cfg.controlMachine}''}
-      ${optionalString (cfg.controlAddr != null) ''controlAddr=${cfg.controlAddr}''}
+      ${optionalString (cfg.controlMachine != null) "controlMachine=${cfg.controlMachine}"}
+      ${optionalString (cfg.controlAddr != null) "controlAddr=${cfg.controlAddr}"}
       ${toString (map (x: "NodeName=${x}\n") cfg.nodeName)}
       ${toString (map (x: "PartitionName=${x}\n") cfg.partitionName)}
       PlugStackConfig=${plugStackConfig}/plugstack.conf
@@ -25,7 +25,7 @@ let
 
   plugStackConfig = pkgs.writeTextDir "plugstack.conf"
     ''
-      ${optionalString cfg.enableSrunX11 ''optional ${pkgs.slurm-spank-x11}/lib/x11.so''}
+      ${optionalString cfg.enableSrunX11 "optional ${pkgs.slurm-spank-x11}/lib/x11.so"}
       ${cfg.extraPlugstackConfig}
     '';
 
@@ -34,13 +34,12 @@ let
      ${cfg.extraCgroupConfig}
    '';
 
-  slurmdbdConf = pkgs.writeTextDir "slurmdbd.conf"
+  slurmdbdConf = pkgs.writeText "slurmdbd.conf"
    ''
      DbdHost=${cfg.dbdserver.dbdHost}
      SlurmUser=${cfg.user}
      StorageType=accounting_storage/mysql
      StorageUser=${cfg.dbdserver.storageUser}
-     ${optionalString (cfg.dbdserver.storagePass != null) "StoragePass=${cfg.dbdserver.storagePass}"}
      ${cfg.dbdserver.extraConfig}
    '';
 
@@ -95,26 +94,12 @@ in
           '';
         };
 
-        storagePass = mkOption {
-          type = types.nullOr types.str;
+        storagePassFile = mkOption {
+          type = with types; nullOr str;
           default = null;
           description = ''
-            Database password. Note that this password will be publicable
-            readable in the nix store. Use <option>configFile</option>
-            to store the and config file and password outside the nix store.
-          '';
-        };
-
-        configFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = ''
-            Path to <literal>slurmdbd.conf</literal>. The password for the database connection
-            is stored in the config file. Use this option to specfify a path
-            outside the nix store. If this option is unset a configuration file
-            will be generated. See also:
-            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
-            <manvolnum>8</manvolnum></citerefentry>.
+            Path to file with database password. The content of this will be used to
+            create the password for the <literal>StoragePass</literal> option.
           '';
         };
 
@@ -122,7 +107,9 @@ in
           type = types.lines;
           default = "";
           description = ''
-            Extra configuration for <literal>slurmdbd.conf</literal>
+            Extra configuration for <literal>slurmdbd.conf</literal> See also:
+            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
+            <manvolnum>8</manvolnum></citerefentry>.
           '';
         };
       };
@@ -292,6 +279,16 @@ in
 
   };
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "storagePass" ] ''
+      This option has been removed so that the database password is not exposed via the nix store.
+      Use services.slurm.dbdserver.storagePassFile to provide the database password.
+    '')
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "configFile" ] ''
+      This option has been removed. Use services.slurm.dbdserver.storagePassFile
+      and services.slurm.dbdserver.extraConfig instead.
+    '')
+  ];
 
   ###### implementation
 
@@ -386,23 +383,34 @@ in
       '';
     };
 
-    systemd.services.slurmdbd = mkIf (cfg.dbdserver.enable) {
+    systemd.services.slurmdbd = let
+      # slurm strips the last component off the path
+      configPath = "$RUNTIME_DIRECTORY/slurmdbd.conf";
+    in mkIf (cfg.dbdserver.enable) {
       path = with pkgs; [ wrappedSlurm munge coreutils ];
 
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" "munged.service" "mysql.service" ];
       requires = [ "munged.service" "mysql.service" ];
 
-      # slurm strips the last component off the path
-      environment.SLURM_CONF =
-        if (cfg.dbdserver.configFile == null) then
-          "${slurmdbdConf}/slurm.conf"
-        else
-          cfg.dbdserver.configFile;
+      preStart = ''
+        cp ${slurmdbdConf} ${configPath}
+        chmod 600 ${configPath}
+        chown ${cfg.user} ${configPath}
+        ${optionalString (cfg.dbdserver.storagePassFile != null) ''
+          echo "StoragePass=$(cat ${cfg.dbdserver.storagePassFile})" \
+            >> ${configPath}
+        ''}
+      '';
+
+      script = ''
+        export SLURM_CONF=${configPath}
+        exec ${cfg.package}/bin/slurmdbd -D
+      '';
 
       serviceConfig = {
-        Type = "forking";
-        ExecStart = "${cfg.package}/bin/slurmdbd";
+        RuntimeDirectory = "slurmdbd";
+        Type = "simple";
         PIDFile = "/run/slurmdbd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
       };
diff --git a/nixos/modules/services/computing/torque/mom.nix b/nixos/modules/services/computing/torque/mom.nix
index 0c5f43cf3e6a2..6747bd4b0d5aa 100644
--- a/nixos/modules/services/computing/torque/mom.nix
+++ b/nixos/modules/services/computing/torque/mom.nix
@@ -32,7 +32,7 @@ in
     environment.systemPackages = [ pkgs.torque ];
 
     systemd.services.torque-mom-init = {
-      path = with pkgs; [ torque utillinux procps inetutils ];
+      path = with pkgs; [ torque util-linux procps inetutils ];
 
       script = ''
         pbs_mkdirs -v aux
diff --git a/nixos/modules/services/computing/torque/server.nix b/nixos/modules/services/computing/torque/server.nix
index 21c5a4f46724d..8d923fc04d46d 100644
--- a/nixos/modules/services/computing/torque/server.nix
+++ b/nixos/modules/services/computing/torque/server.nix
@@ -21,7 +21,7 @@ in
     environment.systemPackages = [ pkgs.torque ];
 
     systemd.services.torque-server-init = {
-      path = with pkgs; [ torque utillinux procps inetutils ];
+      path = with pkgs; [ torque util-linux procps inetutils ];
 
       script = ''
         tmpsetup=$(mktemp -t torque-XXXX)
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index e1950b91382bd..a49f5f8100dc9 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -223,6 +223,7 @@ in {
       };
 
       pythonPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = pythonPackages: with pythonPackages; [ ];
         defaultText = "pythonPackages: with pythonPackages; [ ]";
         description = "Packages to add the to the PYTHONPATH of the buildbot process.";
@@ -282,5 +283,5 @@ in {
     '')
   ];
 
-  meta.maintainers = with lib.maintainers; [ nand0p mic92 ];
+  meta.maintainers = with lib.maintainers; [ nand0p mic92 lopsided98 ];
 }
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 431555309cc9c..2c6d9530a6b86 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -66,10 +66,10 @@ let
             ++ optional service.debugTraceDisabled
             "--debug-trace-disabled"
             ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
-            ++ optionals (service.executor == "docker") (
+            ++ optionals (hasPrefix "docker" service.executor) (
               assert (
                 assertMsg (service.dockerImage != null)
-                  "dockerImage option is required for docker executor (${name})");
+                  "dockerImage option is required for ${service.executor} executor (${name})");
               [ "--docker-image ${service.dockerImage}" ]
               ++ optional service.dockerDisableCache
               "--docker-disable-cache"
@@ -541,7 +541,7 @@ in
         jq
         moreutils
         remarshal
-        utillinux
+        util-linux
         cfg.package
       ] ++ cfg.extraPackages;
       reloadIfChanged = true;
diff --git a/nixos/modules/services/continuous-integration/gocd-agent/default.nix b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
index 2e9e1c94857a0..8cae08bf1fa02 100644
--- a/nixos/modules/services/continuous-integration/gocd-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
@@ -90,6 +90,7 @@ in {
       };
 
       startupOptions = mkOption {
+        type = types.listOf types.str;
         default = [
           "-Xms${cfg.initialJavaHeapSize}"
           "-Xmx${cfg.maxJavaHeapMemory}"
@@ -105,6 +106,7 @@ in {
 
       extraOptions = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [
           "-X debug"
           "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
index 4fa41ac49edfc..4c829664a0a5c 100644
--- a/nixos/modules/services/continuous-integration/gocd-server/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -27,6 +27,7 @@ in {
 
       extraGroups = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [ "wheel" "docker" ];
         description = ''
           List of extra groups that the "gocd-server" user should be a part of.
@@ -92,6 +93,7 @@ in {
       };
 
       startupOptions = mkOption {
+        type = types.listOf types.str;
         default = [
           "-Xms${cfg.initialJavaHeapSize}"
           "-Xmx${cfg.maxJavaHeapMemory}"
@@ -113,6 +115,7 @@ in {
 
       extraOptions = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [
           "-X debug"
           "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
index 4aed493c0fb0a..9f9b86ee61cbb 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -7,14 +7,21 @@ Platform-specific code is in the respective default.nix files.
  */
 
 { config, lib, options, pkgs, ... }:
-
 let
-  inherit (lib) mkOption mkIf types filterAttrs literalExample mkRenamedOptionModule;
+  inherit (lib)
+    filterAttrs
+    literalExample
+    mkIf
+    mkOption
+    mkRemovedOptionModule
+    mkRenamedOptionModule
+    types
+    ;
 
   cfg =
     config.services.hercules-ci-agent;
 
-  format = pkgs.formats.toml {};
+  format = pkgs.formats.toml { };
 
   settingsModule = { config, ... }: {
     freeformType = format.type;
@@ -28,10 +35,14 @@ let
       };
       concurrentTasks = mkOption {
         description = ''
-          Number of tasks to perform simultaneously, such as evaluations, derivations.
+          Number of tasks to perform simultaneously.
+
+          A task is a single derivation build or an evaluation.
+          At minimum, you need 2 concurrent tasks for <literal>x86_64-linux</literal>
+          in your cluster, to allow for import from derivation.
 
-          You must have a total capacity across agents of at least 2 concurrent tasks on <literal>x86_64-linux</literal>
-          to allow for import from derivation.
+          <literal>concurrentTasks</literal> can be around the CPU core count or lower if memory is
+          the bottleneck.
         '';
         type = types.int;
         default = 4;
@@ -77,61 +88,39 @@ let
     };
   };
 
+  # TODO (roberth, >=2022) remove
   checkNix =
     if !cfg.checkNix
     then ""
-    else if lib.versionAtLeast config.nix.package.version "2.4.0"
+    else if lib.versionAtLeast config.nix.package.version "2.3.10"
     then ""
-    else pkgs.stdenv.mkDerivation {
-      name = "hercules-ci-check-system-nix-src";
-      inherit (config.nix.package) src patches;
-      configurePhase = ":";
-      buildPhase = ''
-        echo "Checking in-memory pathInfoCache expiry"
-        if ! grep 'struct PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
-          cat 1>&2 <<EOF
-
-          You are deploying Hercules CI Agent on a system with an incompatible
-          nix-daemon. Please
-           - either upgrade Nix to version 2.4.0 (when released),
-           - or set option services.hercules-ci-agent.patchNix = true;
-           - or set option nix.package to a build of Nix 2.3 with this patch applied:
-               https://github.com/NixOS/nix/pull/3405
-
-          The patch is required for Nix-daemon clients that expect a change in binary
-          cache contents while running, like the agent's evaluator. Without it, import
-          from derivation will fail if your cluster has more than one machine.
-          We are conservative with changes to the overall system, which is why we
-          keep changes to a minimum and why we ask for confirmation in the form of
-          services.hercules-ci-agent.patchNix = true before applying.
-
-        EOF
-          exit 1
-        fi
-      '';
-      installPhase = "touch $out";
-    };
+    else
+      pkgs.stdenv.mkDerivation {
+        name = "hercules-ci-check-system-nix-src";
+        inherit (config.nix.package) src patches;
+        configurePhase = ":";
+        buildPhase = ''
+          echo "Checking in-memory pathInfoCache expiry"
+          if ! grep 'PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
+            cat 1>&2 <<EOF
+
+            You are deploying Hercules CI Agent on a system with an incompatible
+            nix-daemon. Please make sure nix.package is set to a Nix version of at
+            least 2.3.10 or a master version more recent than Mar 12, 2020.
+          EOF
+            exit 1
+          fi
+        '';
+        installPhase = "touch $out";
+      };
 
-  patchedNix = lib.mkIf (!lib.versionAtLeast pkgs.nix.version "2.4.0") (
-    if lib.versionAtLeast pkgs.nix.version "2.4pre"
-    then lib.warn "Hercules CI Agent module will not patch 2.4 pre-release. Make sure it includes (equivalently) PR #3043, commit d048577909 or is no older than 2020-03-13." pkgs.nix
-    else pkgs.nix.overrideAttrs (
-      o: {
-        patches = (o.patches or []) ++ [ backportNix3398 ];
-      }
-    )
-  );
-
-  backportNix3398 = pkgs.fetchurl {
-    url = "https://raw.githubusercontent.com/hercules-ci/hercules-ci-agent/hercules-ci-agent-0.7.3/for-upstream/issue-3398-path-info-cache-ttls-backport-2.3.patch";
-    sha256 = "0jfckqjir9il2il7904yc1qyadw366y7xqzg81sp9sl3f1pw70ib";
-  };
 in
 {
   imports = [
-    (mkRenamedOptionModule ["services" "hercules-ci-agent" "extraOptions"] ["services" "hercules-ci-agent" "settings"])
-    (mkRenamedOptionModule ["services" "hercules-ci-agent" "baseDirectory"] ["services" "hercules-ci-agent" "settings" "baseDirectory"])
-    (mkRenamedOptionModule ["services" "hercules-ci-agent" "concurrentTasks"] ["services" "hercules-ci-agent" "settings" "concurrentTasks"])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "extraOptions" ] [ "services" "hercules-ci-agent" "settings" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "baseDirectory" ] [ "services" "hercules-ci-agent" "settings" "baseDirectory" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "concurrentTasks" ] [ "services" "hercules-ci-agent" "settings" "concurrentTasks" ])
+    (mkRemovedOptionModule [ "services" "hercules-ci-agent" "patchNix" ] "Nix versions packaged in this version of Nixpkgs don't need a patched nix-daemon to work correctly in Hercules CI Agent clusters.")
   ];
 
   options.services.hercules-ci-agent = {
@@ -147,15 +136,6 @@ in
         Support is available at <link xlink:href="mailto:help@hercules-ci.com">help@hercules-ci.com</link>.
       '';
     };
-    patchNix = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Fix Nix 2.3 cache path metadata caching behavior. Has the effect of <literal>nix.package = patch pkgs.nix;</literal>
-
-        This option will be removed when Hercules CI Agent moves to Nix 2.4 (upcoming Nix release).
-      '';
-    };
     checkNix = mkOption {
       type = types.bool;
       default = true;
@@ -206,7 +186,6 @@ in
       # even shortly after the previous lookup. This *also* applies to the daemon.
       narinfo-cache-negative-ttl = 0
     '';
-    nix.package = mkIf cfg.patchNix patchedNix;
     services.hercules-ci-agent.tomlFile =
       format.generate "hercules-ci-agent.toml" cfg.settings;
   };
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
index d2e7e8e18f94e..e8a42e59de0d0 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -7,9 +7,7 @@ Code that is shared with nix-darwin goes in common.nix.
  */
 
 { pkgs, config, lib, ... }:
-
 let
-
   inherit (lib) mkIf mkDefault;
 
   cfg = config.services.hercules-ci-agent;
@@ -21,23 +19,22 @@ in
 {
   imports = [
     ./common.nix
-    (lib.mkRenamedOptionModule ["services" "hercules-ci-agent" "user"] ["systemd" "services" "hercules-ci-agent" "serviceConfig" "User"])
+    (lib.mkRenamedOptionModule [ "services" "hercules-ci-agent" "user" ] [ "systemd" "services" "hercules-ci-agent" "serviceConfig" "User" ])
   ];
 
   config = mkIf cfg.enable {
-
     systemd.services.hercules-ci-agent = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
       wants = [ "network-online.target" ];
       path = [ config.nix.package ];
+      startLimitBurst = 30 * 1000000; # practically infinite
       serviceConfig = {
         User = "hercules-ci-agent";
         ExecStart = command;
         ExecStartPre = testCommand;
         Restart = "on-failure";
         RestartSec = 120;
-        StartLimitBurst = 30 * 1000000; # practically infinite
       };
     };
 
@@ -81,6 +78,8 @@ in
       isSystemUser = true;
     };
 
-    users.groups.hercules-ci-agent = {};
+    users.groups.hercules-ci-agent = { };
   };
+
+  meta.maintainers = [ lib.maintainers.roberth ];
 }
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 502a5898a5de5..887a0cbf9a7ab 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -37,8 +37,6 @@ let
 
   haveLocalDB = cfg.dbi == localDB;
 
-  inherit (config.system) stateVersion;
-
   hydra-package =
   let
     makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
@@ -96,7 +94,8 @@ in
 
       package = mkOption {
         type = types.package;
-        defaultText = "pkgs.hydra";
+        default = pkgs.hydra-unstable;
+        defaultText = "pkgs.hydra-unstable";
         description = "The Hydra package.";
       };
 
@@ -225,34 +224,6 @@ in
 
   config = mkIf cfg.enable {
 
-    warnings = optional (cfg.package.migration or false) ''
-      You're currently deploying an older version of Hydra which is needed to
-      make some required database changes[1]. As soon as this is done, it's recommended
-      to run `hydra-backfill-ids` and set `services.hydra.package` to `pkgs.hydra-unstable`
-      after that.
-
-      [1] https://github.com/NixOS/hydra/pull/711
-    '';
-
-    services.hydra.package = with pkgs;
-      mkDefault (
-        if pkgs ? hydra
-          then throw ''
-            The Hydra package doesn't exist anymore in `nixpkgs`! It probably exists
-            due to an overlay. To upgrade Hydra, you need to take two steps as some
-            bigger changes in the database schema were implemented recently[1]. You first
-            need to deploy `pkgs.hydra-migration`, run `hydra-backfill-ids` on the server
-            and then deploy `pkgs.hydra-unstable`.
-
-            If you want to use `pkgs.hydra` from your overlay, please set `services.hydra.package`
-            explicitly to `pkgs.hydra` and make sure you know what you're doing.
-
-            [1] https://github.com/NixOS/hydra/pull/711
-          ''
-        else if versionOlder stateVersion "20.03" then hydra-migration
-        else hydra-unstable
-      );
-
     users.groups.hydra = {
       gid = config.ids.gids.hydra;
     };
@@ -260,7 +231,7 @@ in
     users.users.hydra =
       { description = "Hydra";
         group = "hydra";
-        createHome = true;
+        # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
         home = baseDir;
         useDefaultShell = true;
         uid = config.ids.uids.hydra;
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
index 1477c471f8ab1..cdc3b4b5c58f5 100644
--- a/nixos/modules/services/continuous-integration/jenkins/default.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -86,8 +86,8 @@ in {
       };
 
       packages = mkOption {
-        default = [ pkgs.stdenv pkgs.git pkgs.jdk config.programs.ssh.package pkgs.nix ];
-        defaultText = "[ pkgs.stdenv pkgs.git pkgs.jdk config.programs.ssh.package pkgs.nix ]";
+        default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ];
+        defaultText = "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]";
         type = types.listOf types.package;
         description = ''
           Packages to add to PATH for the jenkins process.
@@ -207,7 +207,7 @@ in {
 
       # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
       script = ''
-        ${pkgs.jdk}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
+        ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
                                                   --httpPort=${toString cfg.port} \
                                                   --prefix=${cfg.prefix} \
                                                   -Djava.awt.headless=true \
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index f385331e87823..c99a7529213d7 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -16,8 +16,7 @@ let
       [admins]
       ${cfg.adminUser} = ${cfg.adminPass}
     '' else
-    ''
-    '') + (if useVersion2 then
+    "") + (if useVersion2 then
     ''
       [chttpd]
     '' else
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
index 95837aa1cea66..ed47f647edd36 100644
--- a/nixos/modules/services/databases/firebird.nix
+++ b/nixos/modules/services/databases/firebird.nix
@@ -59,6 +59,7 @@ in
 
       port = mkOption {
         default = "3050";
+        type = types.port;
         description = ''
           Port Firebird uses.
         '';
@@ -66,6 +67,7 @@ in
 
       user = mkOption {
         default = "firebird";
+        type = types.str;
         description = ''
           User account under which firebird runs.
         '';
@@ -73,6 +75,7 @@ in
 
       baseDir = mkOption {
         default = "/var/db/firebird"; # ubuntu is using /var/lib/firebird/2.1/data/.. ?
+        type = types.str;
         description = ''
           Location containing data/ and system/ directories.
           data/ stores the databases, system/ stores the password database security2.fdb.
@@ -114,7 +117,7 @@ in
         serviceConfig.User = cfg.user;
         serviceConfig.LogsDirectory = "firebird";
         serviceConfig.LogsDirectoryMode = "0700";
-        serviceConfig.ExecStart = ''${firebird}/bin/fbserver -d'';
+        serviceConfig.ExecStart = "${firebird}/bin/fbserver -d";
 
         # TODO think about shutdown
       };
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
index 18727acc7c756..e22127403e91c 100644
--- a/nixos/modules/services/databases/foundationdb.nix
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -233,7 +233,7 @@ in
             type = types.str;
             default = "Check.Valid=1,Check.Unexpired=1";
             description = ''
-	      "Peer verification string". This may be used to adjust which TLS
+              "Peer verification string". This may be used to adjust which TLS
               client certificates a server will accept, as a form of user
               authorization; for example, it may only accept TLS clients who
               offer a certificate abiding by some locality or organization name.
diff --git a/nixos/modules/services/databases/memcached.nix b/nixos/modules/services/databases/memcached.nix
index f54bb6cc9b180..ca7b20eb049af 100644
--- a/nixos/modules/services/databases/memcached.nix
+++ b/nixos/modules/services/databases/memcached.nix
@@ -17,39 +17,44 @@ in
   options = {
 
     services.memcached = {
-
       enable = mkEnableOption "Memcached";
 
       user = mkOption {
+        type = types.str;
         default = "memcached";
         description = "The user to run Memcached as";
       };
 
       listen = mkOption {
+        type = types.str;
         default = "127.0.0.1";
-        description = "The IP address to bind to";
+        description = "The IP address to bind to.";
       };
 
       port = mkOption {
+        type = types.port;
         default = 11211;
-        description = "The port to bind to";
+        description = "The port to bind to.";
       };
 
       enableUnixSocket = mkEnableOption "unix socket at /run/memcached/memcached.sock";
 
       maxMemory = mkOption {
+        type = types.ints.unsigned;
         default = 64;
         description = "The maximum amount of memory to use for storage, in megabytes.";
       };
 
       maxConnections = mkOption {
+        type = types.ints.unsigned;
         default = 1024;
-        description = "The maximum number of simultaneous connections";
+        description = "The maximum number of simultaneous connections.";
       };
 
       extraOptions = mkOption {
+        type = types.listOf types.str;
         default = [];
-        description = "A list of extra options that will be added as a suffix when running memcached";
+        description = "A list of extra options that will be added as a suffix when running memcached.";
       };
     };
 
diff --git a/nixos/modules/services/databases/mongodb.nix b/nixos/modules/services/databases/mongodb.nix
index 4453a182990d1..db1e5fedf50d8 100644
--- a/nixos/modules/services/databases/mongodb.nix
+++ b/nixos/modules/services/databases/mongodb.nix
@@ -41,16 +41,19 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "mongodb";
         description = "User account under which MongoDB runs";
       };
 
       bind_ip = mkOption {
+        type = types.str;
         default = "127.0.0.1";
         description = "IP to bind to";
       };
 
       quiet = mkOption {
+        type = types.bool;
         default = false;
         description = "quieter output";
       };
@@ -68,16 +71,19 @@ in
       };
 
       dbpath = mkOption {
+        type = types.str;
         default = "/var/db/mongodb";
         description = "Location where MongoDB stores its files";
       };
 
       pidFile = mkOption {
+        type = types.str;
         default = "/run/mongodb.pid";
         description = "Location of MongoDB pid file";
       };
 
       replSetName = mkOption {
+        type = types.str;
         default = "";
         description = ''
           If this instance is part of a replica set, set its name here.
@@ -86,6 +92,7 @@ in
       };
 
       extraConfig = mkOption {
+        type = types.lines;
         default = "";
         example = ''
           storage.journal.enabled: false
@@ -176,7 +183,7 @@ in
         postStart = ''
             if test -e "${cfg.dbpath}/.first_startup"; then
               ${optionalString (cfg.initialScript != null) ''
-                ${mongodb}/bin/mongo -u root -p ${cfg.initialRootPassword} admin "${cfg.initialScript}"
+                ${mongodb}/bin/mongo ${optionalString (cfg.enableAuth) "-u root -p ${cfg.initialRootPassword}"} admin "${cfg.initialScript}"
               ''}
               rm -f "${cfg.dbpath}/.first_startup"
             fi
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index 09b453e758451..53760bb24c4a4 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -16,14 +16,14 @@ let
       ''}
       dbms.ssl.policy.${name}.client_auth=${conf.clientAuth}
       ${if length (splitString "/" conf.privateKey) > 1 then
-        ''dbms.ssl.policy.${name}.private_key=${conf.privateKey}''
+        "dbms.ssl.policy.${name}.private_key=${conf.privateKey}"
       else
-        ''dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}''
+        "dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}"
       }
       ${if length (splitString "/" conf.privateKey) > 1 then
-        ''dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}''
+        "dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}"
       else
-        ''dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}''
+        "dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}"
       }
       dbms.ssl.policy.${name}.revoked_dir=${conf.revokedDir}
       dbms.ssl.policy.${name}.tls_versions=${concatStringsSep "," conf.tlsVersions}
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 7472538b887e3..f0efc659cff74 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -1,43 +1,121 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.openldap;
+  legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
   openldap = cfg.package;
-
-  dataFile = pkgs.writeText "ldap-contents.ldif" cfg.declarativeContents;
-  configFile = pkgs.writeText "slapd.conf" ((optionalString cfg.defaultSchemas ''
-    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}
-    suffix ${cfg.suffix}
-    rootdn ${cfg.rootdn}
-    ${if (cfg.rootpw != null) then ''
-      rootpw ${cfg.rootpw}
-    '' else ''
-      include ${cfg.rootpwFile}
-    ''}
-    directory ${cfg.dataDir}
-    ${cfg.extraDatabaseConfig}
-  '');
-  configOpts = if cfg.configDir == null then "-f ${configFile}"
-               else "-F ${cfg.configDir}";
-in
-
-{
-
-  ###### interface
-
+  configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
+
+  ldapValueType = let
+    # Can't do types.either with multiple non-overlapping submodules, so define our own
+    singleLdapValueType = lib.mkOptionType rec {
+      name = "LDAP";
+      description = "LDAP value";
+      check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
+      merge = lib.mergeEqualOption;
+    };
+    # We don't coerce to lists of single values, as some values must be unique
+  in types.either singleLdapValueType (types.listOf singleLdapValueType);
+
+  ldapAttrsType =
+    let
+      options = {
+        attrs = mkOption {
+          type = types.attrsOf ldapValueType;
+          default = {};
+          description = "Attributes of the parent entry.";
+        };
+        children = mkOption {
+          # Hide the child attributes, to avoid infinite recursion in e.g. documentation
+          # Actual Nix evaluation is lazy, so this is not an issue there
+          type = let
+            hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
+          in types.attrsOf (types.submodule { options = hiddenOptions; });
+          default = {};
+          description = "Child entries of the current entry, with recursively the same structure.";
+          example = lib.literalExample ''
+            {
+                "cn=schema" = {
+                # The attribute used in the DN must be defined
+                attrs = { cn = "schema"; };
+                children = {
+                    # This entry's DN is expanded to "cn=foo,cn=schema"
+                    "cn=foo" = { ... };
+                };
+                # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
+                includes = [ ... ];
+                };
+            }
+          '';
+        };
+        includes = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            LDIF files to include after the parent's attributes but before its children.
+          '';
+        };
+      };
+    in types.submodule { inherit options; };
+
+  valueToLdif = attr: values: let
+    listValues = if lib.isList values then values else lib.singleton values;
+  in map (value:
+    if lib.isAttrs value then
+      if lib.hasAttr "path" value
+      then "${attr}:< file://${value.path}"
+      else "${attr}:: ${value.base64}"
+    else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
+  ) listValues;
+
+  attrsToLdif = dn: { attrs, children, includes, ... }: [''
+    dn: ${dn}
+    ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
+  ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
+    lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
+  );
+in {
+  imports = let
+    deprecationNote = "This option is removed due to the deprecation of `slapd.conf` upstream. Please migrate to `services.openldap.settings`, see the release notes for advice with this process.";
+    mkDatabaseOption = old: new:
+      lib.mkChangedOptionModule [ "services" "openldap" old ] [ "services" "openldap" "settings" "children" ]
+        (config: let
+          database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+          value = lib.getAttrFromPath [ "services" "openldap" old ] config;
+        in lib.setAttrByPath ([ "olcDatabase={1}${database}" "attrs" ] ++ new) value);
+  in [
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "logLevel" ] [ "services" "openldap" "settings" "attrs" "olcLogLevel" ]
+      (config: lib.splitString " " (lib.getAttrFromPath [ "services" "openldap" "logLevel" ] config)))
+    (lib.mkChangedOptionModule [ "services" "openldap" "defaultSchemas" ] [ "services" "openldap" "settings" "children" "cn=schema" "includes"]
+      (config: lib.optionals (lib.getAttrFromPath [ "services" "openldap" "defaultSchemas" ] config) (
+        map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ])))
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "database" ] [ "services" "openldap" "settings" "children" ]
+      (config: let
+        database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+      in {
+        "olcDatabase={1}${database}".attrs = {
+          # objectClass is case-insensitive, so don't need to capitalize ${database}
+          objectClass = [ "olcdatabaseconfig" "olc${database}config" ];
+          olcDatabase = "{1}${database}";
+          olcDbDirectory = lib.mkDefault "/var/db/openldap";
+        };
+        "cn=schema".includes = lib.mkDefault (
+          map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ]
+        );
+      }))
+    (mkDatabaseOption "rootpwFile" [ "olcRootPW" "path" ])
+    (mkDatabaseOption "suffix" [ "olcSuffix" ])
+    (mkDatabaseOption "dataDir" [ "olcDbDirectory" ])
+    (mkDatabaseOption "rootdn" [ "olcRootDN" ])
+    (mkDatabaseOption "rootpw" [ "olcRootPW" ])
+  ];
   options = {
-
     services.openldap = {
-
       enable = mkOption {
         type = types.bool;
         default = false;
@@ -77,224 +155,170 @@ in
         example = [ "ldaps:///" ];
       };
 
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/openldap";
-        description = "The database directory.";
-      };
-
-      defaultSchemas = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Include the default schemas core, cosine, inetorgperson and nis.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      database = mkOption {
-        type = types.str;
-        default = "mdb";
-        description = ''
-          Database type to use for the LDAP.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      suffix = mkOption {
-        type = types.str;
-        example = "dc=example,dc=org";
-        description = ''
-          Specify the DN suffix of queries that will be passed to this backend
-          database.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      rootdn = mkOption {
-        type = types.str;
-        example = "cn=admin,dc=example,dc=org";
-        description = ''
-          Specify the distinguished name that is not subject to access control
-          or administrative limit restrictions for operations on this database.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      rootpw = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Password for the root user.
-          This setting will be ignored if configDir is set.
-          Using this option will store the root password in plain text in the
-          world-readable nix store. To avoid this the <literal>rootpwFile</literal> can be used.
+      settings = mkOption {
+        type = ldapAttrsType;
+        description = "Configuration for OpenLDAP, in OLC format";
+        example = lib.literalExample ''
+          {
+            attrs.olcLogLevel = [ "stats" ];
+            children = {
+              "cn=schema".includes = [
+                 "\${pkgs.openldap}/etc/schema/core.ldif"
+                 "\${pkgs.openldap}/etc/schema/cosine.ldif"
+                 "\${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              ];
+              "olcDatabase={-1}frontend" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{-1}frontend";
+                  olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
+                };
+              };
+              "olcDatabase={0}config" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{0}config";
+                  olcAccess = [ "{0}to * by * none break" ];
+                };
+              };
+              "olcDatabase={1}mdb" = {
+                attrs = {
+                  objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                  olcDatabase = "{1}mdb";
+                  olcDbDirectory = "/var/db/ldap";
+                  olcDbIndex = [
+                    "objectClass eq"
+                    "cn pres,eq"
+                    "uid pres,eq"
+                    "sn pres,eq,subany"
+                  ];
+                  olcSuffix = "dc=example,dc=com";
+                  olcAccess = [ "{0}to * by * read break" ];
+                };
+              };
+            };
+          };
         '';
       };
 
-      rootpwFile = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Password file for the root user.
-          The file should contain the string <literal>rootpw</literal> followed by the password.
-          e.g.: <literal>rootpw mysecurepassword</literal>
-        '';
-      };
-
-      logLevel = mkOption {
-        type = types.str;
-        default = "0";
-        example = "acl trace";
-        description = "The log level selector of slapd.";
-      };
-
+      # This option overrides settings
       configDir = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "Use this optional config directory instead of using slapd.conf";
+        description = ''
+          Use this config directory instead of generating one from the
+          <literal>settings</literal> option. Overrides all NixOS settings. If
+          you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
+        '';
         example = "/var/db/slapd.d";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "
-          slapd.conf configuration
-        ";
-        example = literalExample ''
-            '''
-            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
-            rootdn cn=admin,dc=example,dc=org
-            # NOTE: change after first start
-            rootpw secret
-            directory /var/db/openldap
-            '''
-          '';
-      };
-
       declarativeContents = mkOption {
-        type = with types; nullOr lines;
-        default = null;
+        type = with types; attrsOf lines;
+        default = {};
         description = ''
-          Declarative contents for the LDAP database, in LDIF format.
+          Declarative contents for the LDAP database, in LDIF format by suffix.
 
-          Note a few facts when using it. First, the database
-          <emphasis>must</emphasis> be stored in the directory defined by
-          <code>dataDir</code>. Second, all <code>dataDir</code> will be erased
-          when starting the LDAP server. Third, modifications to the database
-          are not prevented, they are just dropped on the next reboot of the
-          server. Finally, performance-wise the database and indexes are rebuilt
-          on each server startup, so this will slow down server startup,
+          All data will be erased when starting the LDAP server. Modifications
+          to the database are not prevented, they are just dropped on the next
+          reboot of the server. Performance-wise the database and indexes are
+          rebuilt on each server startup, so this will slow down server startup,
           especially with large databases.
         '';
-        example = ''
-          dn: dc=example,dc=org
-          objectClass: domain
-          dc: example
-
-          dn: ou=users,dc=example,dc=org
-          objectClass = organizationalUnit
-          ou: users
-
-          # ...
+        example = lib.literalExample ''
+          {
+            "dc=example,dc=org" = '''
+              dn= dn: dc=example,dc=org
+              objectClass: domain
+              dc: example
+
+              dn: ou=users,dc=example,dc=org
+              objectClass = organizationalUnit
+              ou: users
+
+              # ...
+            ''';
+          }
         '';
       };
-
-      extraDatabaseConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          slapd.conf configuration after the database option.
-          This setting will be ignored if configDir is set.
-        '';
-        example = ''
-          # Indices to maintain for this directory
-          # unique id so equality match only
-          index uid eq
-          # allows general searching on commonname, givenname and email
-          index cn,gn,mail eq,sub
-          # allows multiple variants on surname searching
-          index sn eq,sub
-          # sub above includes subintial,subany,subfinal
-          # optimise department searches
-          index ou eq
-          # if searches will include objectClass uncomment following
-          # index objectClass eq
-          # shows use of default index parameter
-          index default eq,sub
-          # indices missing - uses default eq,sub
-          index telephonenumber
-
-          # other database parameters
-          # read more in slapd.conf reference section
-          cachesize 10000
-          checkpoint 128 15
-        '';
-      };
-
     };
-
-  };
-
-  meta = {
-    maintainers = [ lib.maintainers.mic92 ];
   };
 
-
-  ###### implementation
+  meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
 
   config = mkIf cfg.enable {
-    assertions = [
-      {
-        assertion = cfg.configDir != null || cfg.rootpwFile != null || cfg.rootpw != null;
-        message = "services.openldap: Unless configDir is set, either rootpw or rootpwFile must be set";
-      }
-    ];
-
+    assertions = map (opt: {
+      assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule");
+      message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)";
+    }) legacyOptions;
     environment.systemPackages = [ openldap ];
 
+    # Literal attributes must always be set
+    services.openldap.settings = {
+      attrs = {
+        objectClass = "olcGlobal";
+        cn = "config";
+        olcPidFile = "/run/slapd/slapd.pid";
+      };
+      children."cn=schema".attrs = {
+        cn = "schema";
+        objectClass = "olcSchemaConfig";
+      };
+    };
+
     systemd.services.openldap = {
       description = "LDAP server";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      preStart = ''
+      preStart = let
+        settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
+
+        dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
+        dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
+          (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
+        dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
+        mkLoadScript = dn: let
+          dataDir = lib.escapeShellArg (getAttr dn dataDirs);
+        in  ''
+          rm -rf ${dataDir}/*
+          ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
+          chown -R "${cfg.user}:${cfg.group}" ${dataDir}
+        '';
+      in ''
         mkdir -p /run/slapd
         chown -R "${cfg.user}:${cfg.group}" /run/slapd
-        ${optionalString (cfg.declarativeContents != null) ''
-          rm -Rf "${cfg.dataDir}"
-        ''}
-        mkdir -p "${cfg.dataDir}"
-        ${optionalString (cfg.declarativeContents != null) ''
-          ${openldap.out}/bin/slapadd ${configOpts} -l ${dataFile}
-        ''}
-        chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
 
-        ${openldap}/bin/slaptest ${configOpts}
+        mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+        chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+
+        ${lib.optionalString (cfg.configDir == null) (''
+          rm -Rf ${configDir}/*
+          ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
+        '')}
+        chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
+
+        ${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
+        ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
       '';
-      serviceConfig.ExecStart =
-        "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
-          "-u '${cfg.user}' -g '${cfg.group}' " +
-          "-h '${concatStringsSep " " cfg.urlList}' " +
-          "${configOpts}";
+      serviceConfig = {
+        ExecStart = lib.escapeShellArgs ([
+          "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
+          "-h" (lib.concatStringsSep " " cfg.urlList)
+        ]);
+        Type = "forking";
+        PIDFile = cfg.settings.attrs.olcPidFile;
+      };
     };
 
-    users.users.openldap =
-      { name = cfg.user;
+    users.users = lib.optionalAttrs (cfg.user == "openldap") {
+      openldap = {
         group = cfg.group;
-        uid = config.ids.uids.openldap;
-      };
-
-    users.groups.openldap =
-      { name = cfg.group;
-        gid = config.ids.gids.openldap;
+        isSystemUser = true;
       };
+    };
 
+    users.groups = lib.optionalAttrs (cfg.group == "openldap") {
+      openldap = {};
+    };
   };
 }
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 5056d50153f6d..f582b0592774f 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -69,11 +69,16 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Defines how users authenticate themselves to the server. By
-          default, "trust" access to local users will always be granted
-          along with any other custom options. If you do not want this,
-          set this option using "lib.mkForce" to override this
-          behaviour.
+          Defines how users authenticate themselves to the server. See the
+          <link xlink:href="https://www.postgresql.org/docs/current/auth-pg-hba-conf.html">
+          PostgreSQL documentation for pg_hba.conf</link>
+          for details on the expected format of this option. By default,
+          peer based authentication will be used for users connecting
+          via the Unix socket, and md5 password authentication will be
+          used for users connecting via TCP. Any added rules will be
+          inserted above the default rules. If you'd like to replace the
+          default rules entirely, you can use <function>lib.mkForce</function> in your
+          module.
         '';
       };
 
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index f1777854e141c..117e63662258e 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -4,31 +4,16 @@ with lib;
 
 let
   cfg = config.services.redis;
-  redisBool = b: if b then "yes" else "no";
-  condOption = name: value: if value != null then "${name} ${toString value}" else "";
-
-  redisConfig = pkgs.writeText "redis.conf" ''
-    port ${toString cfg.port}
-    ${condOption "bind" cfg.bind}
-    ${condOption "unixsocket" cfg.unixSocket}
-    daemonize no
-    supervised systemd
-    loglevel ${cfg.logLevel}
-    logfile ${cfg.logfile}
-    syslog-enabled ${redisBool cfg.syslog}
-    databases ${toString cfg.databases}
-    ${concatMapStrings (d: "save ${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}\n") cfg.save}
-    dbfilename dump.rdb
-    dir /var/lib/redis
-    ${if cfg.slaveOf != null then "slaveof ${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}" else ""}
-    ${condOption "masterauth" cfg.masterAuth}
-    ${condOption "requirepass" cfg.requirePass}
-    appendOnly ${redisBool cfg.appendOnly}
-    appendfsync ${cfg.appendFsync}
-    slowlog-log-slower-than ${toString cfg.slowLogLogSlowerThan}
-    slowlog-max-len ${toString cfg.slowLogMaxLen}
-    ${cfg.extraConfig}
-  '';
+
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  redisConfig = pkgs.writeText "redis.conf" (generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
+  } cfg.settings);
 in
 {
   imports = [
@@ -37,6 +22,7 @@ in
     (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
     (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
     (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
+    (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
   ];
 
   ###### interface
@@ -87,9 +73,12 @@ in
 
       bind = mkOption {
         type = with types; nullOr str;
-        default = null; # All interfaces
-        description = "The IP interface to bind to.";
-        example = "127.0.0.1";
+        default = "127.0.0.1";
+        description = ''
+          The IP interface to bind to.
+          <literal>null</literal> means "all interfaces".
+        '';
+        example = "192.0.2.1";
       };
 
       unixSocket = mkOption {
@@ -133,12 +122,29 @@ in
       };
 
       slaveOf = mkOption {
-        default = null; # { ip, port }
-        description = "An attribute set with two attributes: ip and port to which this redis instance acts as a slave.";
+        type = with types; nullOr (submodule ({ ... }: {
+          options = {
+            ip = mkOption {
+              type = str;
+              description = "IP of the Redis master";
+              example = "192.168.1.100";
+            };
+
+            port = mkOption {
+              type = port;
+              description = "port of the Redis master";
+              default = 6379;
+            };
+          };
+        }));
+
+        default = null;
+        description = "IP and port to which this redis instance acts as a slave.";
         example = { ip = "192.168.1.100"; port = 6379; };
       };
 
       masterAuth = mkOption {
+        type = with types; nullOr str;
         default = null;
         description = ''If the master is password protected (using the requirePass configuration)
         it is possible to tell the slave to authenticate before starting the replication synchronization
@@ -188,10 +194,20 @@ in
         description = "Maximum number of items to keep in slow log.";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Extra configuration options for redis.conf.";
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+        default = {};
+        description = ''
+          Redis configuration. Refer to
+          <link xlink:href="https://redis.io/topics/config"/>
+          for details on supported values.
+        '';
+        example = literalExample ''
+          {
+            unixsocketperm = "700";
+            loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
+          }
+        '';
       };
     };
 
@@ -222,6 +238,30 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
+    services.redis.settings = mkMerge [
+      {
+        port = cfg.port;
+        daemonize = false;
+        supervised = "systemd";
+        loglevel = cfg.logLevel;
+        logfile = cfg.logfile;
+        syslog-enabled = cfg.syslog;
+        databases = cfg.databases;
+        save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
+        dbfilename = "dump.rdb";
+        dir = "/var/lib/redis";
+        appendOnly = cfg.appendOnly;
+        appendfsync = cfg.appendFsync;
+        slowlog-log-slower-than = cfg.slowLogLogSlowerThan;
+        slowlog-max-len = cfg.slowLogMaxLen;
+      }
+      (mkIf (cfg.bind != null) { bind = cfg.bind; })
+      (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; })
+      (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${cfg.slaveOf.port}"; })
+      (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
+      (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
+    ];
+
     systemd.services.redis = {
       description = "Redis Server";
 
diff --git a/nixos/modules/services/databases/riak-cs.nix b/nixos/modules/services/databases/riak-cs.nix
deleted file mode 100644
index fa6ac88633185..0000000000000
--- a/nixos/modules/services/databases/riak-cs.nix
+++ /dev/null
@@ -1,202 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.riak-cs;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.riak-cs = {
-
-      enable = mkEnableOption "riak-cs";
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.riak-cs;
-        defaultText = "pkgs.riak-cs";
-        example = literalExample "pkgs.riak-cs";
-        description = ''
-          Riak package to use.
-        '';
-      };
-
-      nodeName = mkOption {
-        type = types.str;
-        default = "riak-cs@127.0.0.1";
-        description = ''
-          Name of the Erlang node.
-        '';
-      };
-
-      anonymousUserCreation = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Anonymous user creation.
-        '';
-      };
-
-      riakHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8087";
-        description = ''
-          Name of riak hosting service.
-        '';
-      };
-
-      listener = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8080";
-        description = ''
-          Name of Riak CS listening service.
-        '';
-      };
-
-      stanchionHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of stanchion hosting service.
-        '';
-      };
-
-      stanchionSsl = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Tell stanchion to use SSL.
-        '';
-      };
-
-      distributedCookie = mkOption {
-        type = types.str;
-        default = "riak";
-        description = ''
-          Cookie for distributed node communication.  All nodes in the
-          same cluster should use the same cookie or they will not be able to
-          communicate.
-        '';
-      };
-
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/riak-cs";
-        description = ''
-          Data directory for Riak CS.
-        '';
-      };
-
-      logDir = mkOption {
-        type = types.path;
-        default = "/var/log/riak-cs";
-        description = ''
-          Log directory for Riak CS.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>riak-cs.conf</filename>.
-        '';
-      };
-
-      extraAdvancedConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>advanced.config</filename>.
-        '';
-      };
-    };
-
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-    environment.etc."riak-cs/riak-cs.conf".text = ''
-      nodename = ${cfg.nodeName}
-      distributed_cookie = ${cfg.distributedCookie}
-
-      platform_log_dir = ${cfg.logDir}
-
-      riak_host = ${cfg.riakHost}
-      listener = ${cfg.listener}
-      stanchion_host = ${cfg.stanchionHost}
-
-      anonymous_user_creation = ${if cfg.anonymousUserCreation then "on" else "off"}
-
-      ${cfg.extraConfig}
-    '';
-
-    environment.etc."riak-cs/advanced.config".text = ''
-      ${cfg.extraAdvancedConfig}
-    '';
-
-    users.users.riak-cs = {
-      name = "riak-cs";
-      uid = config.ids.uids.riak-cs;
-      group = "riak";
-      description = "Riak CS server user";
-    };
-
-  systemd.services.riak-cs = {
-      description = "Riak CS Server";
-
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      path = [
-        pkgs.utillinux # for `logger`
-        pkgs.bash
-      ];
-
-      environment.HOME = "${cfg.dataDir}";
-      environment.RIAK_CS_DATA_DIR = "${cfg.dataDir}";
-      environment.RIAK_CS_LOG_DIR = "${cfg.logDir}";
-      environment.RIAK_CS_ETC_DIR = "/etc/riak";
-
-      preStart = ''
-        if ! test -e ${cfg.logDir}; then
-          mkdir -m 0755 -p ${cfg.logDir}
-          chown -R riak-cs ${cfg.logDir}
-        fi
-
-        if ! test -e ${cfg.dataDir}; then
-          mkdir -m 0700 -p ${cfg.dataDir}
-          chown -R riak-cs ${cfg.dataDir}
-        fi
-      '';
-
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/riak-cs console";
-        ExecStop = "${cfg.package}/bin/riak-cs stop";
-        StandardInput = "tty";
-        User = "riak-cs";
-        Group = "riak-cs";
-        PermissionsStartOnly = true;
-        # Give Riak a decent amount of time to clean up.
-        TimeoutStopSec = 120;
-        LimitNOFILE = 65536;
-      };
-
-      unitConfig.RequiresMountsFor = [
-        "${cfg.dataDir}"
-        "${cfg.logDir}"
-        "/etc/riak"
-      ];
-    };
-  };
-}
diff --git a/nixos/modules/services/databases/riak.nix b/nixos/modules/services/databases/riak.nix
index 885215209bdf4..657eeea87bf4c 100644
--- a/nixos/modules/services/databases/riak.nix
+++ b/nixos/modules/services/databases/riak.nix
@@ -118,7 +118,7 @@ in
       after = [ "network.target" ];
 
       path = [
-        pkgs.utillinux # for `logger`
+        pkgs.util-linux # for `logger`
         pkgs.bash
       ];
 
diff --git a/nixos/modules/services/databases/stanchion.nix b/nixos/modules/services/databases/stanchion.nix
deleted file mode 100644
index 97e55bc70c470..0000000000000
--- a/nixos/modules/services/databases/stanchion.nix
+++ /dev/null
@@ -1,194 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.stanchion;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.stanchion = {
-
-      enable = mkEnableOption "stanchion";
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.stanchion;
-        defaultText = "pkgs.stanchion";
-        example = literalExample "pkgs.stanchion";
-        description = ''
-          Stanchion package to use.
-        '';
-      };
-
-      nodeName = mkOption {
-        type = types.str;
-        default = "stanchion@127.0.0.1";
-        description = ''
-          Name of the Erlang node.
-        '';
-      };
-
-      adminKey = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Name of admin user.
-        '';
-      };
-
-      adminSecret = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Name of admin secret
-        '';
-      };
-
-      riakHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8087";
-        description = ''
-          Name of riak hosting service.
-        '';
-      };
-
-      listener = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of Riak CS listening service.
-        '';
-      };
-
-      stanchionHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of stanchion hosting service.
-        '';
-      };
-
-      distributedCookie = mkOption {
-        type = types.str;
-        default = "riak";
-        description = ''
-          Cookie for distributed node communication.  All nodes in the
-          same cluster should use the same cookie or they will not be able to
-          communicate.
-        '';
-      };
-
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/stanchion";
-        description = ''
-          Data directory for Stanchion.
-        '';
-      };
-
-      logDir = mkOption {
-        type = types.path;
-        default = "/var/log/stanchion";
-        description = ''
-          Log directory for Stanchion.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>stanchion.conf</filename>.
-        '';
-      };
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-
-    environment.etc."stanchion/advanced.config".text = ''
-      [{stanchion, []}].
-    '';
-
-    environment.etc."stanchion/stanchion.conf".text = ''
-      listener = ${cfg.listener}
-
-      riak_host = ${cfg.riakHost}
-
-      ${optionalString (cfg.adminKey == "") "#"} admin.key=${optionalString (cfg.adminKey != "") cfg.adminKey}
-      ${optionalString (cfg.adminSecret == "") "#"} admin.secret=${optionalString (cfg.adminSecret != "") cfg.adminSecret}
-
-      platform_bin_dir = ${pkgs.stanchion}/bin
-      platform_data_dir = ${cfg.dataDir}
-      platform_etc_dir = /etc/stanchion
-      platform_lib_dir = ${pkgs.stanchion}/lib
-      platform_log_dir = ${cfg.logDir}
-
-      nodename = ${cfg.nodeName}
-
-      distributed_cookie = ${cfg.distributedCookie}
-
-      ${cfg.extraConfig}
-    '';
-
-    users.users.stanchion = {
-      name = "stanchion";
-      uid = config.ids.uids.stanchion;
-      group = "stanchion";
-      description = "Stanchion server user";
-    };
-
-    users.groups.stanchion.gid = config.ids.gids.stanchion;
-
-    systemd.tmpfiles.rules = [
-      "d '${cfg.logDir}' - stanchion stanchion --"
-      "d '${cfg.dataDir}' 0700 stanchion stanchion --"
-    ];
-
-    systemd.services.stanchion = {
-      description = "Stanchion Server";
-
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      path = [
-        pkgs.utillinux # for `logger`
-        pkgs.bash
-      ];
-
-      environment.HOME = "${cfg.dataDir}";
-      environment.STANCHION_DATA_DIR = "${cfg.dataDir}";
-      environment.STANCHION_LOG_DIR = "${cfg.logDir}";
-      environment.STANCHION_ETC_DIR = "/etc/stanchion";
-
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/stanchion console";
-        ExecStop = "${cfg.package}/bin/stanchion stop";
-        StandardInput = "tty";
-        User = "stanchion";
-        Group = "stanchion";
-        # Give Stanchion a decent amount of time to clean up.
-        TimeoutStopSec = 120;
-        LimitNOFILE = 65536;
-      };
-
-      unitConfig.RequiresMountsFor = [
-        "${cfg.dataDir}"
-        "${cfg.logDir}"
-        "/etc/stanchion"
-      ];
-    };
-  };
-}
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
index 0af5d2adf3724..5b09115bb2fb0 100644
--- a/nixos/modules/services/databases/victoriametrics.nix
+++ b/nixos/modules/services/databases/victoriametrics.nix
@@ -40,10 +40,10 @@ let cfg = config.services.victoriametrics; in
     systemd.services.victoriametrics = {
       description = "VictoriaMetrics time series database";
       after = [ "network.target" ];
+      startLimitBurst = 5;
       serviceConfig = {
         Restart = "on-failure";
         RestartSec = 1;
-        StartLimitBurst = 5;
         StateDirectory = "victoriametrics";
         DynamicUser = true;
         ExecStart = ''
diff --git a/nixos/modules/services/databases/virtuoso.nix b/nixos/modules/services/databases/virtuoso.nix
index 6eb09e0a58fcb..8b01622ecb039 100644
--- a/nixos/modules/services/databases/virtuoso.nix
+++ b/nixos/modules/services/databases/virtuoso.nix
@@ -16,28 +16,33 @@ with lib;
       enable = mkEnableOption "Virtuoso Opensource database server";
 
       config = mkOption {
+        type = types.lines;
         default = "";
         description = "Extra options to put into Virtuoso configuration file.";
       };
 
       parameters = mkOption {
+        type = types.lines;
         default = "";
         description = "Extra options to put into [Parameters] section of Virtuoso configuration file.";
       };
 
       listenAddress = mkOption {
+        type = types.str;
         default = "1111";
         example = "myserver:1323";
         description = "ip:port or port to listen on.";
       };
 
       httpListenAddress = mkOption {
+        type = types.nullOr types.str;
         default = null;
         example = "myserver:8080";
         description = "ip:port or port for Virtuoso HTTP server to listen on.";
       };
 
       dirsAllowed = mkOption {
+        type = types.nullOr types.str; # XXX Maybe use a list in the future?
         default = null;
         example = "/www, /home/";
         description = "A list of directories Virtuoso is allowed to access";
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
index 7da92cc9f2649..d0f6b66328a4c 100644
--- a/nixos/modules/services/desktops/flatpak.nix
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -15,6 +15,18 @@ in {
   options = {
     services.flatpak = {
       enable = mkEnableOption "flatpak";
+
+      guiPackages = mkOption {
+        internal = true;
+        type = types.listOf types.package;
+        default = [];
+        example = literalExample "[ pkgs.gnome3.gnome-software ]";
+        description = ''
+          Packages that provide an interface for flatpak
+          (like gnome-software) that will be automatically available
+          to all users when flatpak is enabled.
+        '';
+      };
     };
   };
 
@@ -28,7 +40,7 @@ in {
       }
     ];
 
-    environment.systemPackages = [ pkgs.flatpak ];
+    environment.systemPackages = [ pkgs.flatpak ] ++ cfg.guiPackages;
 
     services.dbus.packages = [ pkgs.flatpak ];
 
diff --git a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix b/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
index bd62d16f61ceb..749f12b86bc8a 100644
--- a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
+++ b/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
@@ -15,31 +15,45 @@ with lib;
   options = {
 
     services.gnome3.evolution-data-server = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable Evolution Data Server, a collection of services for
-          storing addressbooks and calendars.
-        '';
+      enable = mkEnableOption "Evolution Data Server, a collection of services for storing addressbooks and calendars.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = "Plugins for Evolution Data Server.";
       };
-
     };
+    programs.evolution = {
+      enable = mkEnableOption "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExample "[ pkgs.evolution-ews ]";
+        description = "Plugins for Evolution.";
+      };
 
+    };
   };
 
-
   ###### implementation
 
-  config = mkIf config.services.gnome3.evolution-data-server.enable {
-
-    environment.systemPackages = [ pkgs.gnome3.evolution-data-server ];
-
-    services.dbus.packages = [ pkgs.gnome3.evolution-data-server ];
-
-    systemd.packages = [ pkgs.gnome3.evolution-data-server ];
-
-  };
-
+  config =
+    let
+      bundle = pkgs.evolutionWithPlugins.override { inherit (config.services.gnome3.evolution-data-server) plugins; };
+    in
+    mkMerge [
+      (mkIf config.services.gnome3.evolution-data-server.enable {
+        environment.systemPackages = [ bundle ];
+
+        services.dbus.packages = [ bundle ];
+
+        systemd.packages = [ bundle ];
+      })
+      (mkIf config.programs.evolution.enable {
+        services.gnome3.evolution-data-server = {
+          enable = true;
+          plugins = [ pkgs.evolution ] ++ config.programs.evolution.plugins;
+        };
+        services.gnome3.gnome-keyring.enable = true;
+      })
+    ];
 }
diff --git a/nixos/modules/services/desktops/pipewire.nix b/nixos/modules/services/desktops/pipewire.nix
deleted file mode 100644
index 5aee59cfdcce9..0000000000000
--- a/nixos/modules/services/desktops/pipewire.nix
+++ /dev/null
@@ -1,41 +0,0 @@
-# pipewire service.
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.pipewire;
-  packages = with pkgs; [ pipewire ];
-
-in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-  };
-
-  ###### interface
-  options = {
-    services.pipewire = {
-      enable = mkEnableOption "pipewire service";
-
-      socketActivation = mkOption {
-        default = true;
-        type = types.bool;
-        description = ''
-          Automatically run pipewire when connections are made to the pipewire socket.
-        '';
-      };
-    };
-  };
-
-
-  ###### implementation
-  config = mkIf cfg.enable {
-    environment.systemPackages = packages;
-
-    systemd.packages = packages;
-
-    systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
-  };
-
-}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
new file mode 100644
index 0000000000000..168cf13a88cb3
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
@@ -0,0 +1,340 @@
+# pipewire example session manager.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pipewire.media-session;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit
+                           && pkgs.stdenv.isx86_64
+                           && pkgs.pkgsi686Linux.pipewire != null;
+
+  # Helpers for generating the pipewire JSON config file
+  mkSPAValueString = v:
+  if builtins.isList v then "[${lib.concatMapStringsSep " " mkSPAValueString v}]"
+  else if lib.types.attrs.check v then
+    "{${lib.concatStringsSep " " (mkSPAKeyValue v)}}"
+  else lib.generators.mkValueStringDefault { } v;
+
+  mkSPAKeyValue = attrs: map (def: def.content) (
+  lib.sortProperties
+    (
+      lib.mapAttrsToList
+        (k: v: lib.mkOrder (v._priority or 1000) "${lib.escape [ "=" ] k} = ${mkSPAValueString (v._content or v)}")
+        attrs
+    )
+  );
+
+  toSPAJSON = attrs: lib.concatStringsSep "\n" (mkSPAKeyValue attrs);
+in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+  options = {
+    services.pipewire.media-session = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Example pipewire session manager";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pipewire.mediaSession;
+        example = literalExample "pkgs.pipewire.mediaSession";
+        description = ''
+          The pipewire-media-session derivation to use.
+        '';
+      };
+
+      config = mkOption {
+        type = types.attrs;
+        description = ''
+          Configuration for the media session core.
+        '';
+        default = {
+          # media-session config file
+          properties = {
+            # Properties to configure the session and some
+            # modules
+            #mem.mlock-all = false;
+            #context.profile.modules = "default,rtkit";
+          };
+
+          spa-libs = {
+            # Mapping from factory name to library.
+            "api.bluez5.*" = "bluez5/libspa-bluez5";
+            "api.alsa.*" = "alsa/libspa-alsa";
+            "api.v4l2.*" = "v4l2/libspa-v4l2";
+            "api.libcamera.*" = "libcamera/libspa-libcamera";
+          };
+
+          modules = {
+            # These are the modules that are enabled when a file with
+            # the key name is found in the media-session.d config directory.
+            # the default bundle is always enabled.
+
+            default = [
+              "flatpak"			# manages flatpak access
+              "portal"			# manage portal permissions
+              "v4l2"			# video for linux udev detection
+              #"libcamera"		# libcamera udev detection
+              "suspend-node"		# suspend inactive nodes
+              "policy-node"		# configure and link nodes
+              #"metadata"		# export metadata API
+              #"default-nodes"		# restore default nodes
+              #"default-profile"	# restore default profiles
+              #"default-routes"		# restore default route
+              #"streams-follow-default"	# move streams when default changes
+              #"alsa-seq"		# alsa seq midi support
+              #"alsa-monitor"		# alsa udev detection
+              #"bluez5"			# bluetooth support
+              #"restore-stream"		# restore stream settings
+            ];
+            "with-audio" = [
+              "metadata"
+              "default-nodes"
+              "default-profile"
+              "default-routes"
+              "alsa-seq"
+              "alsa-monitor"
+            ];
+            "with-alsa" = [
+              "with-audio"
+            ];
+            "with-jack" = [
+              "with-audio"
+            ];
+            "with-pulseaudio" = [
+              "with-audio"
+              "bluez5"
+              "restore-stream"
+              "streams-follow-default"
+            ];
+          };
+        };
+      };
+
+      alsaMonitorConfig = mkOption {
+        type = types.attrs;
+        description = ''
+          Configuration for the alsa monitor.
+        '';
+        default = {
+          # alsa-monitor config file
+          properties = {
+            #alsa.jack-device = true
+          };
+
+          rules = [
+          # an array of matches/actions to evaluate
+          {
+            # rules for matching a device or node. It is an array of
+            # properties that all need to match the regexp. If any of the
+            # matches work, the actions are executed for the object.
+            matches = [
+              {
+                # this matches all cards
+                device.name = "~alsa_card.*";
+              }
+            ];
+            actions = {
+              # actions can update properties on the matched object.
+              update-props = {
+                api.alsa.use-acp = true;
+                #api.alsa.use-ucm = true;
+                #api.alsa.soft-mixer = false;
+                #api.alsa.ignore-dB = false;
+                #device.profile-set = "profileset-name";
+                #device.profile = "default profile name";
+                api.acp.auto-profile = false;
+                api.acp.auto-port = false;
+                #device.nick = "My Device";
+              };
+            };
+          }
+          {
+            matches = [
+              {
+                # matches all sinks
+                node.name = "~alsa_input.*";
+              }
+              {
+                # matches all sources
+                node.name = "~alsa_output.*";
+              }
+            ];
+            actions = {
+              update-props = {
+                #node.nick = 			"My Node";
+                #node.nick = 			null;
+                #priority.driver = 		100;
+                #priority.session = 		100;
+                #node.pause-on-idle = 		false;
+                #resample.quality = 		4;
+                #channelmix.normalize =		false;
+                #channelmix.mix-lfe = 		false;
+                #audio.channels = 		2;
+                #audio.format = 		"S16LE";
+                #audio.rate = 			44100;
+                #audio.position = 		"FL,FR";
+                #api.alsa.period-size =         1024;
+                #api.alsa.headroom =            0;
+                #api.alsa.disable-mmap =        false;
+                #api.alsa.disable-batch =       false;
+              };
+            };
+          }
+          ];
+        };
+      };
+
+      bluezMonitorConfig = mkOption {
+        type = types.attrs;
+        description = ''
+          Configuration for the bluez5 monitor.
+        '';
+        default = {
+          # bluez-monitor config file
+          properties = {
+            # msbc is not expected to work on all headset + adapter combinations.
+            #bluez5.msbc-support = true;
+            #bluez5.sbc-xq-support = true;
+
+            # Enabled headset roles (default: [ hsp_hs hfp_ag ]), this
+            # property only applies to native backend. Currently some headsets
+            # (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag
+            # enabled, disable either hsp_ag or hfp_ag to work around it.
+            #
+            # Supported headset roles: hsp_hs (HSP Headset),
+            #                          hsp_ag (HSP Audio Gateway),
+            #                          hfp_ag (HFP Audio Gateway)
+            #bluez5.headset-roles = [ "hsp_hs" "hsp_ag" "hfp_ag" ];
+
+            # Enabled A2DP codecs (default: all)
+            #bluez5.codecs = [ "sbc" "aac" "ldac" "aptx" "aptx_hd" ];
+          };
+
+          rules = [
+          # an array of matches/actions to evaluate
+          {
+            # rules for matching a device or node. It is an array of
+            # properties that all need to match the regexp. If any of the
+            # matches work, the actions are executed for the object.
+            matches = [
+              {
+                # this matches all cards
+                device.name = "~bluez_card.*";
+              }
+            ];
+            actions = {
+              # actions can update properties on the matched object.
+              update-props = {
+                #device.nick = 			"My Device";
+              };
+            };
+          }
+          {
+            matches = [
+              {
+                # matches all sinks
+                node.name = "~bluez_input.*";
+              }
+              {
+                # matches all sources
+                node.name = "~bluez_output.*";
+              }
+            ];
+            actions = {
+              update-props = {
+                #node.nick = 			"My Node"
+                #node.nick = 			null;
+                #priority.driver = 		100;
+                #priority.session = 		100;
+                #node.pause-on-idle = 		false;
+                #resample.quality = 		4;
+                #channelmix.normalize =		false;
+                #channelmix.mix-lfe = 		false;
+              };
+            };
+          }
+          ];
+        };
+      };
+
+      v4l2MonitorConfig = mkOption {
+        type = types.attrs;
+        description = ''
+          Configuration for the V4L2 monitor.
+        '';
+        default = {
+          # v4l2-monitor config file
+          properties = {
+          };
+
+          rules = [
+            # an array of matches/actions to evaluate
+            {
+              # rules for matching a device or node. It is an array of
+              # properties that all need to match the regexp. If any of the
+              # matches work, the actions are executed for the object.
+              matches = [
+                {
+                  # this matches all devices
+                  device.name = "~v4l2_device.*";
+                }
+              ];
+              actions = {
+                # actions can update properties on the matched object.
+                update-props = {
+                  #device.nick = 			"My Device";
+                };
+              };
+            }
+            {
+              matches = [
+                {
+                  # matches all sinks
+                  node.name = "~v4l2_input.*";
+                }
+                {
+                  # matches all sources
+                  node.name = "~v4l2_output.*";
+                }
+              ];
+              actions = {
+                update-props = {
+                  #node.nick = 			"My Node";
+                  #node.nick = 			null;
+                  #priority.driver = 		100;
+                  #priority.session = 		100;
+                  #node.pause-on-idle = 		true;
+                };
+              };
+            }
+          ];
+        };
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.pipewire.sessionManagerExecutable = "${cfg.package}/bin/pipewire-media-session";
+
+    environment.etc."pipewire/media-session.d/media-session.conf" = { text = toSPAJSON cfg.config; };
+    environment.etc."pipewire/media-session.d/v4l2-monitor.conf" = { text = toSPAJSON cfg.v4l2MonitorConfig; };
+
+    environment.etc."pipewire/media-session.d/with-alsa" = mkIf config.services.pipewire.alsa.enable { text = ""; };
+    environment.etc."pipewire/media-session.d/alsa-monitor.conf" = mkIf config.services.pipewire.alsa.enable { text = toSPAJSON cfg.alsaMonitorConfig; };
+
+    environment.etc."pipewire/media-session.d/with-pulseaudio" = mkIf config.services.pipewire.pulse.enable { text = ""; };
+    environment.etc."pipewire/media-session.d/bluez-monitor.conf" = mkIf config.services.pipewire.pulse.enable { text = toSPAJSON cfg.bluezMonitorConfig; };
+
+    environment.etc."pipewire/media-session.d/with-jack" = mkIf config.services.pipewire.jack.enable { text = ""; };
+  };
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
new file mode 100644
index 0000000000000..044120de7c719
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -0,0 +1,265 @@
+# pipewire service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pipewire;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit
+                           && pkgs.stdenv.isx86_64
+                           && pkgs.pkgsi686Linux.pipewire != null;
+
+  # The package doesn't output to $out/lib/pipewire directly so that the
+  # overlays can use the outputs to replace the originals in FHS environments.
+  #
+  # This doesn't work in general because of missing development information.
+  jack-libs = pkgs.runCommand "jack-libs" {} ''
+    mkdir -p "$out/lib"
+    ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
+  '';
+
+  # Helpers for generating the pipewire JSON config file
+  mkSPAValueString = v:
+  if builtins.isList v then "[${lib.concatMapStringsSep " " mkSPAValueString v}]"
+  else if lib.types.attrs.check v then
+    "{${lib.concatStringsSep " " (mkSPAKeyValue v)}}"
+  else lib.generators.mkValueStringDefault { } v;
+
+  mkSPAKeyValue = attrs: map (def: def.content) (
+  lib.sortProperties
+    (
+      lib.mapAttrsToList
+        (k: v: lib.mkOrder (v._priority or 1000) "${lib.escape [ "=" ] k} = ${mkSPAValueString (v._content or v)}")
+        attrs
+    )
+  );
+
+  toSPAJSON = attrs: lib.concatStringsSep "\n" (mkSPAKeyValue attrs);
+in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+  options = {
+    services.pipewire = {
+      enable = mkEnableOption "pipewire service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pipewire;
+        defaultText = "pkgs.pipewire";
+        example = literalExample "pkgs.pipewire";
+        description = ''
+          The pipewire derivation to use.
+        '';
+      };
+
+      socketActivation = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Automatically run pipewire when connections are made to the pipewire socket.
+        '';
+      };
+
+      config = mkOption {
+        type = types.attrs;
+        description = ''
+          Configuration for the pipewire daemon.
+        '';
+        default = {
+          properties = {
+            ## set-prop is used to configure properties in the system
+            #
+            # "library.name.system" = "support/libspa-support";
+            # "context.data-loop.library.name.system" = "support/libspa-support";
+            "link.max-buffers" = 16; # version < 3 clients can't handle more than 16
+            #"mem.allow-mlock" = false;
+            #"mem.mlock-all" = true;
+            ## https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/src/pipewire/pipewire.h#L93
+            #"log.level" = 2; # 5 is trace, which is verbose as hell, default is 2 which is warnings, 4 is debug output, 3 is info
+
+            ## Properties for the DSP configuration
+            #
+            #"default.clock.rate" = 48000;
+            #"default.clock.quantum" = 1024;
+            #"default.clock.min-quantum" = 32;
+            #"default.clock.max-quantum" = 8192;
+            #"default.video.width" = 640;
+            #"default.video.height" = 480;
+            #"default.video.rate.num" = 25;
+            #"default.video.rate.denom" = 1;
+          };
+
+          spa-libs = {
+            ## add-spa-lib <factory-name regex> <library-name>
+            #
+            # used to find spa factory names. It maps an spa factory name
+            # regular expression to a library name that should contain
+            # that factory.
+            #
+            "audio.convert*" = "audioconvert/libspa-audioconvert";
+            "api.alsa.*" = "alsa/libspa-alsa";
+            "api.v4l2.*" = "v4l2/libspa-v4l2";
+            "api.libcamera.*" = "libcamera/libspa-libcamera";
+            "api.bluez5.*" = "bluez5/libspa-bluez5";
+            "api.vulkan.*" = "vulkan/libspa-vulkan";
+            "api.jack.*" = "jack/libspa-jack";
+            "support.*" = "support/libspa-support";
+            # "videotestsrc" = "videotestsrc/libspa-videotestsrc";
+            # "audiotestsrc" = "audiotestsrc/libspa-audiotestsrc";
+          };
+
+          modules = {
+            ##  <module-name> = { [args = "<key>=<value> ..."]
+            #                     [flags = ifexists] }
+            #                     [flags = [ifexists]|[nofail]}
+            #
+            # Loads a module with the given parameters.
+            # If ifexists is given, the module is ignoed when it is not found.
+            # If nofail is given, module initialization failures are ignored.
+            #
+            libpipewire-module-rtkit = {
+              args = {
+                #rt.prio = 20;
+                #rt.time.soft = 200000;
+                #rt.time.hard = 200000;
+                #nice.level = -11;
+              };
+              flags = "ifexists|nofail";
+            };
+            libpipewire-module-protocol-native = { _priority = -100; _content = "null"; };
+            libpipewire-module-profiler = "null";
+            libpipewire-module-metadata = "null";
+            libpipewire-module-spa-device-factory = "null";
+            libpipewire-module-spa-node-factory = "null";
+            libpipewire-module-client-node = "null";
+            libpipewire-module-client-device = "null";
+            libpipewire-module-portal = "null";
+            libpipewire-module-access = {
+              args.access = {
+                allowed = ["${builtins.unsafeDiscardStringContext cfg.sessionManagerExecutable}"];
+                rejected = [];
+                restricted = [];
+                force = "flatpak";
+              };
+            };
+            libpipewire-module-adapter = "null";
+            libpipewire-module-link-factory = "null";
+            libpipewire-module-session-manager = "null";
+          };
+
+          objects = {
+            ## create-object [-nofail] <factory-name> [<key>=<value> ...]
+            #
+            # Creates an object from a PipeWire factory with the given parameters.
+            # If -nofail is given, errors are ignored (and no object is created)
+            #
+          };
+
+
+          exec = {
+            ## exec <program-name>
+            #
+            # Execute the given program. This is usually used to start the
+            # session manager. run the session manager with -h for options
+            #
+            "${builtins.unsafeDiscardStringContext cfg.sessionManagerExecutable}" = { args = "\"${lib.concatStringsSep " " cfg.sessionManagerArguments}\""; };
+          };
+        };
+      };
+
+      sessionManagerExecutable = mkOption {
+        type = types.str;
+        default = "";
+        example = literalExample ''${pkgs.pipewire.mediaSession}/bin/pipewire-media-session'';
+        description = ''
+          Path to the session manager executable.
+        '';
+      };
+
+      sessionManagerArguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExample ''["-p" "bluez5.msbc-support=true"]'';
+        description = ''
+          Arguments passed to the pipewire session manager.
+        '';
+      };
+
+      alsa = {
+        enable = mkEnableOption "ALSA support";
+        support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems";
+      };
+
+      jack = {
+        enable = mkEnableOption "JACK audio emulation";
+      };
+
+      pulse = {
+        enable = mkEnableOption "PulseAudio server emulation";
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.pulse.enable -> !config.hardware.pulseaudio.enable;
+        message = "PipeWire based PulseAudio server emulation replaces PulseAudio. This option requires `hardware.pulseaudio.enable` to be set to false";
+      }
+      {
+        assertion = cfg.jack.enable -> !config.services.jack.jackd.enable;
+        message = "PipeWire based JACK emulation doesn't use the JACK service. This option requires `services.jack.jackd.enable` to be set to false";
+      }
+    ];
+
+    environment.systemPackages = [ cfg.package ]
+                                 ++ lib.optional cfg.jack.enable jack-libs;
+
+    systemd.packages = [ cfg.package ]
+                       ++ lib.optional cfg.pulse.enable cfg.package.pulse;
+
+    # PipeWire depends on DBUS but doesn't list it. Without this booting
+    # into a terminal results in the service crashing with an error.
+    systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
+    systemd.user.sockets.pipewire-pulse.wantedBy = lib.mkIf (cfg.socketActivation && cfg.pulse.enable) ["sockets.target"];
+    systemd.user.services.pipewire.bindsTo = [ "dbus.service" ];
+    services.udev.packages = [ cfg.package ];
+
+    # If any paths are updated here they must also be updated in the package test.
+    environment.etc."alsa/conf.d/49-pipewire-modules.conf" = mkIf cfg.alsa.enable {
+      text = ''
+        pcm_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;"}
+        }
+        ctl_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;"}
+        }
+      '';
+    };
+    environment.etc."alsa/conf.d/50-pipewire.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf";
+    };
+    environment.etc."alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
+    };
+
+    environment.sessionVariables.LD_LIBRARY_PATH =
+      lib.optional cfg.jack.enable "/run/current-system/sw/lib/pipewire";
+
+    # https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/464#note_723554
+    systemd.user.services.pipewire.environment = {
+      "PIPEWIRE_LINK_PASSIVE" = "1";
+      "PIPEWIRE_CONFIG_FILE" = pkgs.writeText "pipewire.conf" (toSPAJSON cfg.config);
+    };
+  };
+}
diff --git a/nixos/modules/services/desktops/profile-sync-daemon.nix b/nixos/modules/services/desktops/profile-sync-daemon.nix
index a8ac22ac12765..6206295272fc5 100644
--- a/nixos/modules/services/desktops/profile-sync-daemon.nix
+++ b/nixos/modules/services/desktops/profile-sync-daemon.nix
@@ -36,7 +36,7 @@ in {
             description = "Profile Sync daemon";
             wants = [ "psd-resync.service" ];
             wantedBy = [ "default.target" ];
-            path = with pkgs; [ rsync kmod gawk nettools utillinux profile-sync-daemon ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
             unitConfig = {
               RequiresMountsFor = [ "/home/" ];
             };
@@ -55,7 +55,7 @@ in {
             wants = [ "psd-resync.timer" ];
             partOf = [ "psd.service" ];
             wantedBy = [ "default.target" ];
-            path = with pkgs; [ rsync kmod gawk nettools utillinux profile-sync-daemon ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
             serviceConfig = {
               Type = "oneshot";
               ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon resync";
diff --git a/nixos/modules/services/desktops/telepathy.nix b/nixos/modules/services/desktops/telepathy.nix
index 34596bf781849..8c50d860e5bb2 100644
--- a/nixos/modules/services/desktops/telepathy.nix
+++ b/nixos/modules/services/desktops/telepathy.nix
@@ -38,6 +38,11 @@ with lib;
 
     services.dbus.packages = [ pkgs.telepathy-mission-control ];
 
+    # Enable runtime optional telepathy in gnome-shell
+    services.xserver.desktopManager.gnome3.sessionPath = with pkgs; [
+      telepathy-glib
+      telepathy-logger
+    ];
   };
 
 }
diff --git a/nixos/modules/services/development/bloop.nix b/nixos/modules/services/development/bloop.nix
index 226718a9e80ab..c1180a8bbdd46 100644
--- a/nixos/modules/services/development/bloop.nix
+++ b/nixos/modules/services/development/bloop.nix
@@ -44,7 +44,7 @@ in {
       };
       serviceConfig = {
         Type        = "simple";
-        ExecStart   = ''${pkgs.bloop}/bin/bloop server'';
+        ExecStart   = "${pkgs.bloop}/bin/bloop server";
         Restart     = "always";
       };
     };
diff --git a/nixos/modules/services/development/hoogle.nix b/nixos/modules/services/development/hoogle.nix
index 1a98f005602aa..6d6c88b9b2aa9 100644
--- a/nixos/modules/services/development/hoogle.nix
+++ b/nixos/modules/services/development/hoogle.nix
@@ -25,6 +25,7 @@ in {
     };
 
     packages = mkOption {
+      type = types.functionTo (types.listOf types.package);
       default = hp: [];
       defaultText = "hp: []";
       example = "hp: with hp; [ text lens ]";
@@ -49,6 +50,11 @@ in {
       default = "https://hoogle.haskell.org";
     };
 
+    host = mkOption {
+      type = types.str;
+      description = "Set the host to bind on.";
+      default = "127.0.0.1";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -59,12 +65,10 @@ in {
 
       serviceConfig = {
         Restart = "always";
-        ExecStart = ''${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home}'';
+        ExecStart = ''${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home} --host ${cfg.host}'';
 
-        User = "nobody";
-        Group = "nogroup";
+        DynamicUser = true;
 
-        PrivateTmp = true;
         ProtectHome = true;
 
         RuntimeDirectory = "hoogle";
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
index 302aa1ed7c482..fd99ee9442c98 100644
--- a/nixos/modules/services/editors/emacs.xml
+++ b/nixos/modules/services/editors/emacs.xml
@@ -156,7 +156,7 @@ $ ./result/bin/emacs
 
 let
   myEmacs = pkgs.emacs; <co xml:id="ex-emacsNix-2" />
-  emacsWithPackages = (pkgs.emacsPackagesGen myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
+  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
 in
   emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [ <co xml:id="ex-emacsNix-4" />
     magit          # ; Integrate git &lt;C-x g&gt;
@@ -254,10 +254,10 @@ in
     <example xml:id="module-services-emacs-querying-packages">
      <title>Querying Emacs packages</title>
 <programlisting><![CDATA[
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.elpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.melpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.melpaStablePackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.orgPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
 ]]></programlisting>
     </example>
    </para>
diff --git a/nixos/modules/services/editors/infinoted.nix b/nixos/modules/services/editors/infinoted.nix
index 8b997ccbf66e5..10d868b7f1618 100644
--- a/nixos/modules/services/editors/infinoted.nix
+++ b/nixos/modules/services/editors/infinoted.nix
@@ -141,14 +141,14 @@ in {
           install -o ${cfg.user} -g ${cfg.group} -m 0600 /dev/null /var/lib/infinoted/infinoted.conf
           cat >>/var/lib/infinoted/infinoted.conf <<EOF
           [infinoted]
-          ${optionalString (cfg.keyFile != null) ''key-file=${cfg.keyFile}''}
-          ${optionalString (cfg.certificateFile != null) ''certificate-file=${cfg.certificateFile}''}
-          ${optionalString (cfg.certificateChain != null) ''certificate-chain=${cfg.certificateChain}''}
+          ${optionalString (cfg.keyFile != null) "key-file=${cfg.keyFile}"}
+          ${optionalString (cfg.certificateFile != null) "certificate-file=${cfg.certificateFile}"}
+          ${optionalString (cfg.certificateChain != null) "certificate-chain=${cfg.certificateChain}"}
           port=${toString cfg.port}
           security-policy=${cfg.securityPolicy}
           root-directory=${cfg.rootDirectory}
           plugins=${concatStringsSep ";" cfg.plugins}
-          ${optionalString (cfg.passwordFile != null) ''password=$(head -n 1 ${cfg.passwordFile})''}
+          ${optionalString (cfg.passwordFile != null) "password=$(head -n 1 ${cfg.passwordFile})"}
 
           ${cfg.extraConfig}
           EOF
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index 4b2e1a3c07f06..73099ae33634a 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -49,8 +49,13 @@ in
         default = 34197;
         description = ''
           The port to which the service should bind.
-
-          This option will also open up the UDP port in the firewall configuration.
+        '';
+      };
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to automatically open the specified UDP port in the firewall.
         '';
       };
       saveName = mkOption {
@@ -237,6 +242,6 @@ in
       };
     };
 
-    networking.firewall.allowedUDPPorts = [ cfg.port ];
+    networking.firewall.allowedUDPPorts = if cfg.openFirewall then [ cfg.port ] else [];
   };
 }
diff --git a/nixos/modules/services/games/freeciv.nix b/nixos/modules/services/games/freeciv.nix
new file mode 100644
index 0000000000000..4923891a61799
--- /dev/null
+++ b/nixos/modules/services/games/freeciv.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.freeciv;
+  inherit (config.users) groups;
+  rootDir = "/run/freeciv";
+  argsFormat = {
+    type = with lib.types; let
+      valueType = nullOr (oneOf [
+        bool int float str
+        (listOf valueType)
+      ]) // {
+        description = "freeciv-server params";
+      };
+    in valueType;
+    generate = name: value:
+      let mkParam = k: v:
+            if v == null then []
+            else if isBool v then if v then [("--"+k)] else []
+            else [("--"+k) v];
+          mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
+      in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
+  };
+in
+{
+  options = {
+    services.freeciv = {
+      enable = mkEnableOption ''freeciv'';
+      settings = mkOption {
+        description = ''
+          Parameters of freeciv-server.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = argsFormat.type;
+          options.Announce = mkOption {
+            type = types.enum ["IPv4" "IPv6" "none"];
+            default = "none";
+            description = "Announce game in LAN using given protocol.";
+          };
+          options.auth = mkEnableOption "server authentication";
+          options.Database = mkOption {
+            type = types.nullOr types.str;
+            apply = pkgs.writeText "auth.conf";
+            default = ''
+              [fcdb]
+                backend="sqlite"
+                database="/var/lib/freeciv/auth.sqlite"
+            '';
+            description = "Enable database connection with given configuration.";
+          };
+          options.debug = mkOption {
+            type = types.ints.between 0 3;
+            default = 0;
+            description = "Set debug log level.";
+          };
+          options.exit-on-end = mkEnableOption "exit instead of restarting when a game ends.";
+          options.Guests = mkEnableOption "guests to login if auth is enabled";
+          options.Newusers = mkEnableOption "new users to login if auth is enabled";
+          options.port = mkOption {
+            type = types.port;
+            default = 5556;
+            description = "Listen for clients on given port";
+          };
+          options.quitidle = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            description = "Quit if no players for given time in seconds.";
+          };
+          options.read = mkOption {
+            type = types.lines;
+            apply = v: pkgs.writeTextDir "read.serv" v + "/read";
+            default = ''
+              /fcdb lua sqlite_createdb()
+            '';
+            description = "Startup script.";
+          };
+          options.saves = mkOption {
+            type = types.nullOr types.str;
+            default = "/var/lib/freeciv/saves/";
+            description = ''
+              Save games to given directory,
+              a sub-directory named after the starting date of the service
+              will me inserted to preserve older saves.
+            '';
+          };
+        };
+      };
+      openFirewall = mkEnableOption "opening the firewall for the port listening for clients";
+    };
+  };
+  config = mkIf cfg.enable {
+    users.groups.freeciv = {};
+    # Use with:
+    #   journalctl -u freeciv.service -f -o cat &
+    #   cat >/run/freeciv.stdin
+    #   load saves/2020-11-14_05-22-27/freeciv-T0005-Y-3750-interrupted.sav.bz2
+    systemd.sockets.freeciv = {
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenFIFO = "/run/freeciv.stdin";
+        SocketGroup = groups.freeciv.name;
+        SocketMode = "660";
+        RemoveOnStop = true;
+      };
+    };
+    systemd.services.freeciv = {
+      description = "Freeciv Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.HOME = "/var/lib/freeciv";
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = "5s";
+        StandardInput = "fd:freeciv.socket";
+        StandardOutput = "journal";
+        StandardError = "journal";
+        ExecStart = pkgs.writeShellScript "freeciv-server" (''
+          set -eux
+          savedir=$(date +%Y-%m-%d_%H-%M-%S)
+          '' + "${pkgs.freeciv}/bin/freeciv-server"
+          + " " + optionalString (cfg.settings.saves != null)
+            (concatStringsSep " " [ "--saves" "${escapeShellArg cfg.settings.saves}/$savedir" ])
+          + " " + argsFormat.generate "freeciv-server" (cfg.settings // { saves = null; }));
+        DynamicUser = true;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        StateDirectory = [ "freeciv" ];
+        WorkingDirectory = "/var/lib/freeciv";
+        # 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";
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindReadOnlyPaths = [
+          builtins.storeDir
+          "/etc"
+          "/run"
+        ];
+        # The following options are only for optimizing:
+        # systemd-analyze security freeciv
+        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;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "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 -x, 2>perf.log -e 'syscalls:sys_enter_*' freeciv-server
+          # in tests, and seem likely not necessary for freeciv-server.
+          "~@aio" "~@chown" "~@ipc" "~@keyring" "~@memlock"
+          "~@resources" "~@setuid" "~@sync" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+    networking.firewall = mkIf cfg.openFirewall
+      { allowedTCPPorts = [ cfg.settings.port ]; };
+  };
+  meta.maintainers = with lib.maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/games/openarena.nix b/nixos/modules/services/games/openarena.nix
index 8c014d78809b8..9c441e98b2068 100644
--- a/nixos/modules/services/games/openarena.nix
+++ b/nixos/modules/services/games/openarena.nix
@@ -19,7 +19,7 @@ in
       extraFlags = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = ''Extra flags to pass to <command>oa_ded</command>'';
+        description = "Extra flags to pass to <command>oa_ded</command>";
         example = [
           "+set dedicated 2"
           "+set sv_hostname 'My NixOS OpenArena Server'"
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
new file mode 100644
index 0000000000000..72c4eccaff72e
--- /dev/null
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.auto-cpufreq;
+in {
+  options = {
+    services.auto-cpufreq = {
+      enable = mkEnableOption "auto-cpufreq daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.auto-cpufreq ];
+
+    systemd.packages = [ pkgs.auto-cpufreq ];
+    systemd.services.auto-cpufreq.path = with pkgs; [ bash coreutils ];
+  };
+}
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
index dfa39e7f6024c..08ad90126b1d2 100644
--- a/nixos/modules/services/hardware/bluetooth.nix
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -1,12 +1,39 @@
 { config, lib, pkgs, ... }:
-
-with lib;
-
 let
   cfg = config.hardware.bluetooth;
-  bluez-bluetooth = cfg.package;
+  package = cfg.package;
+
+  inherit (lib)
+    mkDefault mkEnableOption mkIf mkOption
+    mkRenamedOptionModule mkRemovedOptionModule
+    concatStringsSep escapeShellArgs
+    optional optionals optionalAttrs recursiveUpdate types;
+
+  cfgFmt = pkgs.formats.ini { };
+
+  # bluez will complain if some of the sections are not found, so just make them
+  # empty (but present in the file) for now
+  defaults = {
+    General.ControllerMode = "dual";
+    Controller = { };
+    GATT = { };
+    Policy.AutoEnable = cfg.powerOnBoot;
+  };
+
+  hasDisabledPlugins = builtins.length cfg.disabledPlugins > 0;
 
-in {
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "bluetooth" "config" ] [ "hardware" "bluetooth" "settings" ])
+    (mkRemovedOptionModule [ "hardware" "bluetooth" "extraConfig" ] ''
+      Use hardware.bluetooth.settings instead.
+
+      This is part of the general move to use structured settings instead of raw
+      text for config as introduced by RFC0042:
+      https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
+    '')
+  ];
 
   ###### interface
 
@@ -15,8 +42,10 @@ in {
     hardware.bluetooth = {
       enable = mkEnableOption "support for Bluetooth";
 
+      hsphfpd.enable = mkEnableOption "support for hsphfpd[-prototype] implementation";
+
       powerOnBoot = mkOption {
-        type    = types.bool;
+        type = types.bool;
         default = true;
         description = "Whether to power up the default Bluetooth controller on boot.";
       };
@@ -36,8 +65,15 @@ in {
         '';
       };
 
-      config = mkOption {
-        type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
+      disabledPlugins = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = "Built-in plugins to disable";
+      };
+
+      settings = mkOption {
+        type = cfgFmt.type;
+        default = { };
         example = {
           General = {
             ControllerMode = "bredr";
@@ -45,55 +81,65 @@ in {
         };
         description = "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
       };
-
-      extraConfig = mkOption {
-        type = with types; nullOr lines;
-        default = null;
-        example = ''
-          [General]
-          ControllerMode = bredr
-        '';
-        description = ''
-          Set additional configuration for system-wide bluetooth (/etc/bluetooth/main.conf).
-        '';
-      };
     };
-
   };
 
   ###### implementation
 
   config = mkIf cfg.enable {
-    warnings = optional (cfg.extraConfig != null) "hardware.bluetooth.`extraConfig` is deprecated, please use hardware.bluetooth.`config`.";
+    environment.systemPackages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
 
-    hardware.bluetooth.config = {
-      Policy = {
-        AutoEnable = mkDefault cfg.powerOnBoot;
-      };
-    };
-
-    environment.systemPackages = [ bluez-bluetooth ];
-
-    environment.etc."bluetooth/main.conf"= {
-      source = pkgs.writeText "main.conf"
-        (generators.toINI { } cfg.config + optionalString (cfg.extraConfig != null) cfg.extraConfig);
-    };
-
-    services.udev.packages = [ bluez-bluetooth ];
-    services.dbus.packages = [ bluez-bluetooth ];
-    systemd.packages       = [ bluez-bluetooth ];
+    environment.etc."bluetooth/main.conf".source =
+      cfgFmt.generate "main.conf" (recursiveUpdate defaults cfg.settings);
+    services.udev.packages = [ package ];
+    services.dbus.packages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
+    systemd.packages = [ package ];
 
     systemd.services = {
-      bluetooth = {
+      bluetooth =
+        let
+          # `man bluetoothd` will refer to main.conf in the nix store but bluez
+          # will in fact load the configuration file at /etc/bluetooth/main.conf
+          # so force it here to avoid any ambiguity and things suddenly breaking
+          # if/when the bluez derivation is changed.
+          args = [ "-f" "/etc/bluetooth/main.conf" ]
+            ++ optional hasDisabledPlugins
+            "--noplugin=${concatStringsSep "," cfg.disabledPlugins}";
+        in
+        {
+          wantedBy = [ "bluetooth.target" ];
+          aliases = [ "dbus-org.bluez.service" ];
+          serviceConfig.ExecStart = [
+            ""
+            "${package}/libexec/bluetooth/bluetoothd ${escapeShellArgs args}"
+          ];
+          # restarting can leave people without a mouse/keyboard
+          unitConfig.X-RestartIfChanged = false;
+        };
+    }
+    // (optionalAttrs cfg.hsphfpd.enable {
+      hsphfpd = {
+        after = [ "bluetooth.service" ];
+        requires = [ "bluetooth.service" ];
         wantedBy = [ "bluetooth.target" ];
-        aliases  = [ "dbus-org.bluez.service" ];
+
+        description = "A prototype implementation used for connecting HSP/HFP Bluetooth devices";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/hsphfpd.pl";
       };
-    };
+    });
 
     systemd.user.services = {
       obex.aliases = [ "dbus-org.bluez.obex.service" ];
-    };
+    }
+    // optionalAttrs cfg.hsphfpd.enable {
+      telephony_client = {
+        wantedBy = [ "default.target" ];
 
+        description = "telephony_client for hsphfpd";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/telephony_client.pl";
+      };
+    };
   };
-
 }
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 222ac8e487eb3..51eca19dca32b 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -11,8 +11,8 @@ let
     "fwupd/daemon.conf" = {
       source = pkgs.writeText "daemon.conf" ''
         [fwupd]
-        BlacklistDevices=${lib.concatStringsSep ";" cfg.blacklistDevices}
-        BlacklistPlugins=${lib.concatStringsSep ";" cfg.blacklistPlugins}
+        DisabledDevices=${lib.concatStringsSep ";" cfg.disabledDevices}
+        DisabledPlugins=${lib.concatStringsSep ";" cfg.disabledPlugins}
       '';
     };
     "fwupd/uefi.conf" = {
@@ -59,21 +59,21 @@ in {
         '';
       };
 
-      blacklistDevices = mkOption {
+      disabledDevices = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad" ];
         description = ''
-          Allow blacklisting specific devices by their GUID
+          Allow disabling specific devices by their GUID
         '';
       };
 
-      blacklistPlugins = mkOption {
+      disabledPlugins = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "udev" ];
         description = ''
-          Allow blacklisting specific plugins
+          Allow disabling specific plugins
         '';
       };
 
@@ -105,11 +105,15 @@ in {
     };
   };
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistDevices"] [ "services" "fwupd" "disabledDevices" ])
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistPlugins"] [ "services" "fwupd" "disabledPlugins" ])
+  ];
 
   ###### implementation
   config = mkIf cfg.enable {
     # Disable test related plug-ins implicitly so that users do not have to care about them.
-    services.fwupd.blacklistPlugins = cfg.package.defaultBlacklistedPlugins;
+    services.fwupd.disabledPlugins = cfg.package.defaultDisabledPlugins;
 
     environment.systemPackages = [ cfg.package ];
 
diff --git a/nixos/modules/services/hardware/lcd.nix b/nixos/modules/services/hardware/lcd.nix
index d78d742cd3185..dc8595ea60cde 100644
--- a/nixos/modules/services/hardware/lcd.nix
+++ b/nixos/modules/services/hardware/lcd.nix
@@ -151,14 +151,13 @@ in with lib; {
         description = "LCDproc - client";
         after = [ "lcdd.service" ];
         wantedBy = [ "lcd.target" ];
+        # Allow restarting for eternity
+        startLimitIntervalSec = lib.mkIf cfg.client.restartForever 0;
         serviceConfig = serviceCfg // {
           ExecStart = "${pkg}/bin/lcdproc -f -c ${clientCfg}";
           # If the server is being restarted at the same time, the client will
           # fail as it cannot connect, so space it out a bit.
           RestartSec = "5";
-          # Allow restarting for eternity
-          StartLimitIntervalSec = lib.mkIf cfg.client.restartForever "0";
-          StartLimitBurst = lib.mkIf cfg.client.restartForever "0";
         };
       };
     };
diff --git a/nixos/modules/services/hardware/power-profiles-daemon.nix b/nixos/modules/services/hardware/power-profiles-daemon.nix
new file mode 100644
index 0000000000000..70b7a72b8bae0
--- /dev/null
+++ b/nixos/modules/services/hardware/power-profiles-daemon.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.power-profiles-daemon;
+  package = pkgs.power-profiles-daemon;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.power-profiles-daemon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable power-profiles-daemon, a DBus daemon that allows
+          changing system behavior based upon user-selected power profiles.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.tlp.enable;
+        message = ''
+          You have set services.power-profiles-daemon.enable = true;
+          which conflicts with services.tlp.enable = true;
+        '';
+      }
+    ];
+
+    services.dbus.packages = [ package ];
+
+    services.udev.packages = [ package ];
+
+    systemd.packages = [ package ];
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
index b344dfc20610f..03070a8f9e7c6 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -148,7 +148,7 @@ in
           # saned needs to distinguish between IPv4 and IPv6 to open matching data sockets.
           BindIPv6Only = "ipv6-only";
           Accept = true;
-          MaxConnections = 1;
+          MaxConnections = 64;
         };
       };
 
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
index ec0457bbd5830..556f6bbb419a2 100644
--- a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
@@ -19,18 +19,16 @@ nix-shell -E 'with import <nixpkgs> { }; brscan4-etc-files.override{netDevices=[
 
 */
 
-with lib;
-
 let
 
   addNetDev = nd: ''
     brsaneconfig4 -a \
     name="${nd.name}" \
     model="${nd.model}" \
-    ${if (hasAttr "nodename" nd && nd.nodename != null) then
+    ${if (lib.hasAttr "nodename" nd && nd.nodename != null) then
       ''nodename="${nd.nodename}"'' else
       ''ip="${nd.ip}"''}'';
-  addAllNetDev = xs: concatStringsSep "\n" (map addNetDev xs);
+  addAllNetDev = xs: lib.concatStringsSep "\n" (map addNetDev xs);
 in
 
 stdenv.mkDerivation {
@@ -61,11 +59,11 @@ stdenv.mkDerivation {
   dontStrip = true;
   dontPatchELF = true;
 
-  meta = {
+  meta = with lib; {
     description = "Brother brscan4 sane backend driver etc files";
     homepage = "http://www.brother.com";
-    platforms = stdenv.lib.platforms.linux;
-    license = stdenv.lib.licenses.unfree;
-    maintainers = with stdenv.lib.maintainers; [ jraygauthier ];
+    platforms = platforms.linux;
+    license = licenses.unfree;
+    maintainers = with maintainers; [ jraygauthier ];
   };
 }
diff --git a/nixos/modules/services/hardware/thermald.nix b/nixos/modules/services/hardware/thermald.nix
index b7be0e89d0c66..aa936ac09d1d1 100644
--- a/nixos/modules/services/hardware/thermald.nix
+++ b/nixos/modules/services/hardware/thermald.nix
@@ -24,32 +24,30 @@ in {
         description = "the thermald manual configuration file.";
       };
 
-      adaptive = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable adaptive mode, only working on kernel versions greater than 5.8.
-          Thermald will detect this itself, safe to enable on kernel versions below 5.8.
-        '';
+      package = mkOption {
+        type = types.package;
+        default = pkgs.thermald;
+        defaultText = "pkgs.thermald";
+        description = "Which thermald package to use.";
       };
     };
   };
 
   ###### implementation
   config = mkIf cfg.enable {
-    services.dbus.packages = [ pkgs.thermald ];
+    services.dbus.packages = [ cfg.package ];
 
     systemd.services.thermald = {
       description = "Thermal Daemon Service";
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.thermald}/sbin/thermald \
+          ${cfg.package}/sbin/thermald \
             --no-daemon \
             ${optionalString cfg.debug "--loglevel=debug"} \
             ${optionalString (cfg.configFile != null) "--config-file ${cfg.configFile}"} \
-            ${optionalString cfg.adaptive "--adaptive"} \
-            --dbus-enable
+            --dbus-enable \
+            --adaptive
         '';
       };
     };
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index 3bda61ed1a938..7a5a7e1c41ce2 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -5,49 +5,95 @@ with lib;
 let
 
   cfg = config.services.thinkfan;
-  configFile = pkgs.writeText "thinkfan.conf" ''
-    # ATTENTION: There is only very basic sanity checking on the configuration.
-    # That means you can set your temperature limits as insane as you like. You
-    # can do anything stupid, e.g. turn off your fan when your CPU reaches 70°C.
-    #
-    # That's why this program is called THINKfan: You gotta think for yourself.
-    #
-    ######################################################################
-    #
-    # IBM/Lenovo Thinkpads (thinkpad_acpi, /proc/acpi/ibm)
-    # ====================================================
-    #
-    # IMPORTANT:
-    #
-    # To keep your HD from overheating, you have to specify a correction value for
-    # the sensor that has the HD's temperature. You need to do this because
-    # thinkfan uses only the highest temperature it can find in the system, and
-    # that'll most likely never be your HD, as most HDs are already out of spec
-    # when they reach 55 °C.
-    # Correction values are applied from left to right in the same order as the
-    # temperatures are read from the file.
-    #
-    # For example:
-    # tp_thermal /proc/acpi/ibm/thermal (0, 0, 10)
-    # will add a fixed value of 10 °C the 3rd value read from that file. Check out
-    # http://www.thinkwiki.org/wiki/Thermal_Sensors to find out how much you may
-    # want to add to certain temperatures.
-
-    ${cfg.fan}
-    ${cfg.sensors}
-
-    #  Syntax:
-    #  (LEVEL, LOW, HIGH)
-    #  LEVEL is the fan level to use (0-7 with thinkpad_acpi)
-    #  LOW is the temperature at which to step down to the previous level
-    #  HIGH is the temperature at which to step up to the next level
-    #  All numbers are integers.
-    #
-
-    ${cfg.levels}
-  '';
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "thinkfan.yaml" cfg.settings;
+  thinkfan = pkgs.thinkfan.override { inherit (cfg) smartSupport; };
+
+  # fan-speed and temperature levels
+  levelType = with types;
+    let
+      tuple = ts: mkOptionType {
+        name = "tuple";
+        merge = mergeOneOption;
+        check = xs: all id (zipListsWith (t: x: t.check x) ts xs);
+        description = "tuple of" + concatMapStrings (t: " (${t.description})") ts;
+      };
+      level = ints.unsigned;
+      special = enum [ "level auto" "level full-speed" "level disengage" ];
+    in
+      tuple [ (either level special) level level ];
+
+  # sensor or fan config
+  sensorType = name: types.submodule {
+    freeformType = types.attrsOf settingsFormat.type;
+    options = {
+      type = mkOption {
+        type = types.enum [ "hwmon" "atasmart" "tpacpi" "nvml" ];
+        description = ''
+          The ${name} type, can be
+          <literal>hwmon</literal> for standard ${name}s,
 
-  thinkfan = pkgs.thinkfan.override { smartSupport = cfg.smartSupport; };
+          <literal>atasmart</literal> to read the temperature via
+          S.M.A.R.T (requires smartSupport to be enabled),
+
+          <literal>tpacpi</literal> for the legacy thinkpac_acpi driver, or
+
+          <literal>nvml</literal> for the (proprietary) nVidia driver.
+        '';
+      };
+      query = mkOption {
+        type = types.str;
+        description = ''
+          The query string used to match one or more ${name}s: can be
+          a fullpath to the temperature file (single ${name}) or a fullpath
+          to a driver directory (multiple ${name}s).
+
+          <note><para>
+            When multiple ${name}s match, the query can be restricted using the
+            <option>name</option> or <option>indices</option> options.
+          </para></note>
+        '';
+      };
+      indices = mkOption {
+        type = with types; nullOr (listOf ints.unsigned);
+        default = null;
+        description = ''
+          A list of ${name}s to pick in case multiple ${name}s match the query.
+
+          <note><para>Indices start from 0.</para></note>
+        '';
+      };
+    } // optionalAttrs (name == "sensor") {
+      correction = mkOption {
+        type = with types; nullOr (listOf int);
+        default = null;
+        description = ''
+          A list of values to be added to the temperature of each sensor,
+          can be used to equalize small discrepancies in temperature ratings.
+        '';
+      };
+    };
+  };
+
+  # removes NixOS special and unused attributes
+  sensorToConf = { type, query, ... }@args:
+    (filterAttrs (k: v: v != null && !(elem k ["type" "query"])) args)
+    // { "${type}" = query; };
+
+  syntaxNote = name: ''
+    <note><para>
+      This section slightly departs from the thinkfan.conf syntax.
+      The type and path must be specified like this:
+      <literal>
+        type = "tpacpi";
+        query = "/proc/acpi/ibm/${name}";
+      </literal>
+      instead of a single declaration like:
+      <literal>
+        - tpacpi: /proc/acpi/ibm/${name}
+      </literal>
+    </para></note>
+  '';
 
 in {
 
@@ -59,76 +105,93 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable thinkfan, fan controller for IBM/Lenovo ThinkPads.
+          Whether to enable thinkfan, a fan control program.
+
+          <note><para>
+            This module targets IBM/Lenovo thinkpads by default, for
+            other hardware you will have configure it more carefully.
+          </para></note>
         '';
+        relatedPackages = [ "thinkfan" ];
       };
 
       smartSupport = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Whether to build thinkfan with SMART support to read temperatures
+          Whether to build thinkfan with S.M.A.R.T. support to read temperatures
           directly from hard disks.
         '';
       };
 
       sensors = mkOption {
-        type = types.lines;
-        default = ''
-          tp_thermal /proc/acpi/ibm/thermal (0,0,10)
-        '';
-        description =''
-          thinkfan can read temperatures from three possible sources:
-
-            /proc/acpi/ibm/thermal
-              Which is provided by the thinkpad_acpi kernel
-              module (keyword tp_thermal)
-
-            /sys/class/hwmon/*/temp*_input
-              Which may be provided by any hwmon drivers (keyword
-              hwmon)
-
-            S.M.A.R.T. (requires smartSupport to be enabled)
-              Which reads the temperature directly from the hard
-              disk using libatasmart (keyword atasmart)
-
-          Multiple sensors may be added, in which case they will be
-          numbered in their order of appearance.
-        '';
+        type = types.listOf (sensorType "sensor");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/thermal";
+          }
+        ];
+        description = ''
+          List of temperature sensors thinkfan will monitor.
+        '' + syntaxNote "thermal";
       };
 
-      fan = mkOption {
-        type = types.str;
-        default = "tp_fan /proc/acpi/ibm/fan";
-        description =''
-          Specifies the fan we want to use.
-          On anything other than a Thinkpad you'll probably
-          use some PWM control file in /sys/class/hwmon.
-          A sysfs fan would be specified like this:
-            pwm_fan /sys/class/hwmon/hwmon2/device/pwm1
-        '';
+      fans = mkOption {
+        type = types.listOf (sensorType "fan");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/fan";
+          }
+        ];
+        description = ''
+          List of fans thinkfan will control.
+        '' + syntaxNote "fan";
       };
 
       levels = mkOption {
-        type = types.lines;
-        default = ''
-          (0,     0,      55)
-          (1,     48,     60)
-          (2,     50,     61)
-          (3,     52,     63)
-          (6,     56,     65)
-          (7,     60,     85)
-          (127,   80,     32767)
-        '';
+        type = types.listOf levelType;
+        default = [
+          [0  0   55]
+          [1  48  60]
+          [2  50  61]
+          [3  52  63]
+          [6  56  65]
+          [7  60  85]
+          ["level auto" 80 32767]
+        ];
         description = ''
-          (LEVEL, LOW, HIGH)
-          LEVEL is the fan level to use (0-7 with thinkpad_acpi).
+          [LEVEL LOW HIGH]
+
+          LEVEL is the fan level to use: it can be an integer (0-7 with thinkpad_acpi),
+          "level auto" (to keep the default firmware behavior), "level full-speed" or
+          "level disengage" (to run the fan as fast as possible).
           LOW is the temperature at which to step down to the previous level.
           HIGH is the temperature at which to step up to the next level.
           All numbers are integers.
         '';
       };
 
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-b" "0" ];
+        description = ''
+          A list of extra command line arguments to pass to thinkfan.
+          Check the thinkfan(1) manpage for available arguments.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingsFormat.type;
+        default = { };
+        description = ''
+          Thinkfan settings. Use this option to configure thinkfan
+          settings not exposed in a NixOS option or to bypass one.
+          Before changing this, read the <literal>thinkfan.conf(5)</literal>
+          manpage and take a look at the example config file at
+          <link xlink:href="https://github.com/vmatare/thinkfan/blob/master/examples/thinkfan.yaml"/>
+        '';
+      };
 
     };
 
@@ -138,12 +201,21 @@ in {
 
     environment.systemPackages = [ thinkfan ];
 
-    systemd.services.thinkfan = {
-      description = "Thinkfan";
-      after = [ "basic.target" ];
-      wantedBy = [ "multi-user.target" ];
-      path = [ thinkfan ];
-      serviceConfig.ExecStart = "${thinkfan}/bin/thinkfan -n -c ${configFile}";
+    services.thinkfan.settings = mapAttrs (k: v: mkDefault v) {
+      sensors = map sensorToConf cfg.sensors;
+      fans    = map sensorToConf cfg.fans;
+      levels  = cfg.levels;
+    };
+
+    systemd.packages = [ thinkfan ];
+
+    systemd.services = {
+      thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs);
+
+      # must be added manually, see issue #81138
+      thinkfan.wantedBy = [ "multi-user.target" ];
+      thinkfan-wakeup.wantedBy = [ "sleep.target" ];
+      thinkfan-sleep.wantedBy = [ "sleep.target" ];
     };
 
     boot.extraModprobeConfig = "options thinkpad_acpi experimental=1 fan_control=1";
diff --git a/nixos/modules/services/hardware/throttled.nix b/nixos/modules/services/hardware/throttled.nix
index 7617c4492d7c6..1905eb565c6dc 100644
--- a/nixos/modules/services/hardware/throttled.nix
+++ b/nixos/modules/services/hardware/throttled.nix
@@ -26,5 +26,11 @@ in {
       if cfg.extraConfig != ""
       then pkgs.writeText "lenovo_fix.conf" cfg.extraConfig
       else "${pkgs.throttled}/etc/lenovo_fix.conf";
+
+    # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
+    # See https://github.com/erpalma/throttled/issues/215
+    boot.kernelParams =
+      optional (versionAtLeast config.boot.kernelPackages.kernel.version "5.9")
+      "msr.allow_writes=on";
   };
 }
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
index 4230f2edd2793..eb53f565a67f0 100644
--- a/nixos/modules/services/hardware/tlp.nix
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -39,7 +39,7 @@ in
         default = "";
         description = ''
           Verbatim additional configuration variables for TLP.
-          DEPRECATED: use services.tlp.config instead.
+          DEPRECATED: use services.tlp.settings instead.
         '';
       };
     };
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
index 2594ac7437109..8c609bbf825b6 100644
--- a/nixos/modules/services/hardware/trezord.nix
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -47,7 +47,7 @@ in {
     services.udev.packages = [ pkgs.trezor-udev-rules ];
 
     systemd.services.trezord = {
-      description = "TREZOR Bridge";
+      description = "Trezor Bridge";
       after = [ "systemd-udev-settle.service" "network.target" ];
       wantedBy = [ "multi-user.target" ];
       path = [];
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 587b9b0234aa4..d48b5444677c1 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -57,8 +57,8 @@ let
         substituteInPlace $i \
           --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
           --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
-          --replace \"/sbin/blkid \"${pkgs.utillinux}/sbin/blkid \
-          --replace \"/bin/mount \"${pkgs.utillinux}/bin/mount \
+          --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
+          --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
           --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
           --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
       done
@@ -202,10 +202,24 @@ in
         '';
       };
 
+      initrdRules = mkOption {
+        default = "";
+        example = ''
+          SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
+        '';
+        type = types.lines;
+        description = ''
+          <command>udev</command> rules to include in the initrd
+          <emphasis>only</emphasis>. They'll be written into file
+          <filename>99-local.rules</filename>. Thus they are read and applied
+          after the essential initrd rules.
+        '';
+      };
+
       extraRules = mkOption {
         default = "";
         example = ''
-          KERNEL=="eth*", ATTR{address}=="00:1D:60:B9:6D:4F", NAME="my_fast_network_card"
+          ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
         '';
         type = types.lines;
         description = ''
@@ -280,10 +294,17 @@ in
 
     services.udev.packages = [ extraUdevRules extraHwdbFile ];
 
-    services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.utillinux udev ];
+    services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
 
     boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
 
+    boot.initrd.extraUdevRulesCommands = optionalString (cfg.initrdRules != "")
+      ''
+        cat <<'EOF' > $out/99-local.rules
+        ${cfg.initrdRules}
+        EOF
+      '';
+
     environment.etc =
       {
         "udev/rules.d".source = udevRules;
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index 054ffa35050a9..9c2f78a755ddd 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -3,7 +3,12 @@
 with lib;
 let
   cfg = config.services.undervolt;
-  cliArgs = lib.cli.toGNUCommandLineShell {} {
+
+  mkPLimit = limit: window:
+    if (isNull limit && isNull window) then null
+    else assert asserts.assertMsg (!isNull limit && !isNull window) "Both power limit and window must be set";
+      "${toString limit} ${toString window}";
+  cliArgs = lib.cli.toGNUCommandLine {} {
     inherit (cfg)
       verbose
       temp
@@ -21,6 +26,9 @@ let
 
     temp-bat = cfg.tempBat;
     temp-ac = cfg.tempAc;
+
+    power-limit-long = mkPLimit cfg.p1.limit cfg.p1.window;
+    power-limit-short = mkPLimit cfg.p2.limit cfg.p2.window;
   };
 in
 {
@@ -104,6 +112,40 @@ in
       '';
     };
 
+    p1.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P1 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p1.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P1 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
+    p2.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P2 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p2.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P2 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
     useTimer = mkOption {
       type = types.bool;
       default = false;
@@ -133,7 +175,7 @@ in
       serviceConfig = {
         Type = "oneshot";
         Restart = "no";
-        ExecStart = "${pkgs.undervolt}/bin/undervolt ${cliArgs}";
+        ExecStart = "${pkgs.undervolt}/bin/undervolt ${toString cliArgs}";
       };
     };
 
diff --git a/nixos/modules/services/hardware/xow.nix b/nixos/modules/services/hardware/xow.nix
index a18d60ad83bec..311181176bd83 100644
--- a/nixos/modules/services/hardware/xow.nix
+++ b/nixos/modules/services/hardware/xow.nix
@@ -10,7 +10,10 @@ in {
   config = lib.mkIf cfg.enable {
     hardware.uinput.enable = true;
 
+    boot.extraModprobeConfig = lib.readFile "${pkgs.xow}/lib/modprobe.d/xow-blacklist.conf";
+
     systemd.packages = [ pkgs.xow ];
+    systemd.services.xow.wantedBy = [ "multi-user.target" ];
 
     services.udev.packages = [ pkgs.xow ];
   };
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index bf92425f998bb..a4fc315d080d7 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -100,7 +100,7 @@ in
 
       inputConfig = mkOption {
         type = types.lines;
-        default = ''generator { }'';
+        default = "generator { }";
         description = "Logstash input configuration.";
         example = ''
           # Read from journal
@@ -131,7 +131,7 @@ in
 
       outputConfig = mkOption {
         type = types.lines;
-        default = ''stdout { codec => rubydebug }'';
+        default = "stdout { codec => rubydebug }";
         description = "Logstash output configuration.";
         example = ''
           redis { host => ["localhost"] data_type => "list" key => "logstash" codec => json }
diff --git a/nixos/modules/services/logging/promtail.nix b/nixos/modules/services/logging/promtail.nix
new file mode 100644
index 0000000000000..19b12daa41528
--- /dev/null
+++ b/nixos/modules/services/logging/promtail.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.promtail;
+
+  prettyJSON = conf: pkgs.runCommandLocal "promtail-config.json" {} ''
+    echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq 'del(._module)' > $out
+  '';
+
+  allowSystemdJournal = cfg.configuration ? scrape_configs && lib.any (v: v ? journal) cfg.configuration.scrape_configs;
+in {
+  options.services.promtail = with types; {
+    enable = mkEnableOption "the Promtail ingresser";
+
+
+    configuration = mkOption {
+      type = (pkgs.formats.json {}).type;
+      description = ''
+        Specify the configuration for Promtail in Nix.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = listOf str;
+      default = [];
+      example = [ "--server.http-listen-port=3101" ];
+      description = ''
+        Specify a list of additional command line flags,
+        which get escaped and are then passed to Loki.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.promtail.configuration.positions.filename = mkDefault "/var/cache/promtail/positions.yaml";
+
+    systemd.services.promtail = {
+      description = "Promtail log ingress";
+      wantedBy = [ "multi-user.target" ];
+      stopIfChanged = false;
+
+      serviceConfig = {
+        Restart = "on-failure";
+
+        ExecStart = "${pkgs.grafana-loki}/bin/promtail -config.file=${prettyJSON cfg.configuration} ${escapeShellArgs cfg.extraFlags}";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        CacheDirectory = "promtail";
+
+        User = "promtail";
+        Group = "promtail";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        ProtectKernelModules = true;
+        SystemCallArchitectures = "native";
+        ProtectKernelLogs = true;
+        ProtectClock = true;
+
+        LockPersonality = true;
+        ProtectHostname = true;
+        RestrictRealtime = true;
+        MemoryDenyWriteExecute = true;
+        PrivateUsers = true;
+
+        SupplementaryGroups = lib.optional (allowSystemdJournal) "systemd-journal";
+      } // (optionalAttrs (!pkgs.stdenv.isAarch64) { # FIXME: figure out why this breaks on aarch64
+        SystemCallFilter = "@system-service";
+      });
+    };
+
+    users.groups.promtail = {};
+    users.users.promtail = {
+      description = "Promtail service user";
+      isSystemUser = true;
+      group = "promtail";
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
new file mode 100644
index 0000000000000..a7c54ad75fdef
--- /dev/null
+++ b/nixos/modules/services/logging/vector.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.vector;
+
+in {
+  options.services.vector = {
+    enable = mkEnableOption "Vector";
+
+    journaldAccess = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable Vector to access journald.
+      '';
+    };
+
+    settings = mkOption {
+      type = (pkgs.formats.json { }).type;
+      default = { };
+      description = ''
+        Specify the configuration for Vector in Nix.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups.vector = { };
+    users.users.vector = {
+      description = "Vector service user";
+      group = "vector";
+      isSystemUser = true;
+    };
+    systemd.services.vector = {
+      description = "Vector event and log aggregator";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      requires = [ "network-online.target" ];
+      serviceConfig = let
+        format = pkgs.formats.toml { };
+        conf = format.generate "vector.toml" cfg.settings;
+        validateConfig = file:
+          pkgs.runCommand "validate-vector-conf" { } ''
+            ${pkgs.vector}/bin/vector validate --no-topology --no-environment "${file}"
+            ln -s "${file}" "$out"
+          '';
+      in {
+        ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
+        User = "vector";
+        Group = "vector";
+        Restart = "no";
+        StateDirectory = "vector";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        # This group is required for accessing journald.
+        SupplementaryGroups = mkIf cfg.journaldAccess "systemd-journal";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index f5c5f795dc1b6..a2298152b023f 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -427,12 +427,12 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ cfg.configFile modulesDir ];
 
+      startLimitIntervalSec = 60;  # 1 min
       serviceConfig = {
         ExecStart = "${dovecotPkg}/sbin/dovecot -F";
         ExecReload = "${dovecotPkg}/sbin/doveadm reload";
         Restart = "on-failure";
         RestartSec = "1s";
-        StartLimitInterval = "1min";
         RuntimeDirectory = [ "dovecot2" ];
       };
 
@@ -463,7 +463,7 @@ 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."
+      "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
     ];
 
     assertions = [
diff --git a/nixos/modules/services/mail/freepops.nix b/nixos/modules/services/mail/freepops.nix
deleted file mode 100644
index 5b729ca50a5e4..0000000000000
--- a/nixos/modules/services/mail/freepops.nix
+++ /dev/null
@@ -1,89 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.mail.freepopsd;
-in
-
-{
-  options = {
-    services.mail.freepopsd = {
-      enable = mkOption {
-        default = false;
-        type = with types; bool;
-        description = ''
-          Enables Freepops, a POP3 webmail wrapper.
-        '';
-      };
-
-      port = mkOption {
-        default = 2000;
-        type = with types; uniq int;
-        description = ''
-          Port on which the pop server will listen.
-        '';
-      };
-
-      threads = mkOption {
-        default = 5;
-        type = with types; uniq int;
-        description = ''
-          Max simultaneous connections.
-        '';
-      };
-
-      bind = mkOption {
-        default = "0.0.0.0";
-        type = types.str;
-        description = ''
-          Bind over an IPv4 address instead of any.
-        '';
-      };
-
-      logFile = mkOption {
-        default = "/var/log/freepopsd";
-        example = "syslog";
-        type = types.str;
-        description = ''
-          Filename of the log file or syslog to rely on the logging daemon.
-        '';
-      };
-
-      suid = {
-        user = mkOption {
-          default = "nobody";
-          type = types.str;
-          description = ''
-            User name under which freepopsd will be after binding the port.
-          '';
-        };
-
-        group = mkOption {
-          default = "nogroup";
-          type = types.str;
-          description = ''
-            Group under which freepopsd will be after binding the port.
-          '';
-        };
-      };
-
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.freepopsd = {
-      description = "Freepopsd (webmail over POP3)";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      script = ''
-        ${pkgs.freepops}/bin/freepopsd \
-          -p ${toString cfg.port} \
-          -t ${toString cfg.threads} \
-          -b ${cfg.bind} \
-          -vv -l ${cfg.logFile} \
-          -s ${cfg.suid.user}.${cfg.suid.group}
-      '';
-    };
-  };
-}
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 5c61cfbebf6cd..832b496f31c96 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -38,7 +38,7 @@ let
   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" ''
+  postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
     [postfix]
     postmap_command: ${pkgs.postfix}/bin/postmap
     transport_file_type: hash
@@ -81,7 +81,7 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = "Enable Mailman on this host. Requires an active Postfix installation.";
+        description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
       };
 
       package = mkOption {
@@ -92,6 +92,20 @@ in {
         description = "Mailman package to use";
       };
 
+      enablePostfix = mkOption {
+        type = types.bool;
+        default = true;
+        example = false;
+        description = ''
+          Enable Postfix integration. Requires an active Postfix installation.
+
+          If you want to use another MTA, set this option to false and configure
+          settings in services.mailman.settings.mta.
+
+          Refer to the Mailman manual for more info.
+        '';
+      };
+
       siteOwner = mkOption {
         type = types.str;
         example = "postmaster@example.org";
@@ -182,7 +196,7 @@ in {
         pid_file = "/run/mailman/master.pid";
       };
 
-      mta.configuration = lib.mkDefault "${mtaConfig}";
+      mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
 
       "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
         class = "mailman_hyperkitty.Archiver";
@@ -211,14 +225,22 @@ in {
               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
             '';
           };
-    in [
+    in (lib.optionals cfg.enablePostfix [
       { assertion = postfix.enable;
-        message = "Mailman requires Postfix";
+        message = ''
+          Mailman's default NixOS configuration requires Postfix to be enabled.
+
+          If you want to use another MTA, set services.mailman.enablePostfix
+          to false and configure settings in services.mailman.settings.mta.
+
+          Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
+          for more info.
+        '';
       }
       (requirePostfixHash [ "relayDomains" ] "postfix_domains")
       (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
-    ];
+    ]);
 
     users.users.mailman = {
       description = "GNU Mailman";
@@ -275,7 +297,7 @@ in {
       '';
     }) ];
 
-    services.postfix = {
+    services.postfix = lib.mkIf cfg.enablePostfix {
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
       config = {
         owner_request_special = "no";   # Mailman handles -owner addresses on its own
@@ -345,7 +367,7 @@ in {
 
       mailman-web-setup = {
         description = "Prepare mailman-web files and database";
-        before = [ "uwsgi.service" "mailman-uwsgi.service" ];
+        before = [ "mailman-uwsgi.service" ];
         requiredBy = [ "mailman-uwsgi.service" ];
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         script = ''
@@ -421,7 +443,7 @@ in {
         inherit startAt;
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman-web runjobs minutely";
+          ExecStart = "${pythonEnv}/bin/mailman-web runjobs ${name}";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
index cbe50ed0b9179..8da491ccbe9f6 100644
--- a/nixos/modules/services/mail/mailman.xml
+++ b/nixos/modules/services/mail/mailman.xml
@@ -13,9 +13,9 @@
   </para>
 
   <section xml:id="module-services-mailman-basic-usage">
-    <title>Basic usage</title>
+    <title>Basic usage with Postfix</title>
     <para>
-      For a basic configuration, the following settings are suggested:
+      For a basic configuration with Postfix as the MTA, the following settings are suggested:
       <programlisting>{ config, ... }: {
   services.postfix = {
     enable = true;
@@ -56,4 +56,39 @@
       necessary, but outside the scope of the Mailman module.
     </para>
   </section>
+  <section xml:id="module-services-mailman-other-mtas">
+    <title>Using with other MTAs</title>
+    <para>
+      Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
+      <programlisting>{ config, ... }: {
+  services = {
+    mailman = {
+      enable = true;
+      siteOwner = "mailman@example.org";
+      <link linkend="opt-services.mailman.enablePostfix">enablePostfix</link> = false;
+      settings.mta = {
+        incoming = "mailman.mta.exim4.LMTP";
+        outgoing = "mailman.mta.deliver.deliver";
+        lmtp_host = "localhost";
+        lmtp_port = "8024";
+        smtp_host = "localhost";
+        smtp_port = "25";
+        configuration = "python:mailman.config.exim4";
+      };
+    };
+    exim = {
+      enable = true;
+      # You can configure Exim in a separate file to reduce configuration.nix clutter
+      config = builtins.readFile ./exim.conf;
+    };
+  };
+}</programlisting>
+    </para>
+    <para>
+      The exim config needs some special additions to work with Mailman. Currently
+      NixOS can't manage Exim config with such granularity. Please refer to
+      <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>
+      for more info on configuring Mailman for working with Exim.
+    </para>
+  </section>
 </chapter>
diff --git a/nixos/modules/services/mail/mlmmj.nix b/nixos/modules/services/mail/mlmmj.nix
index d58d93c4214c1..fd74f2dc5f07f 100644
--- a/nixos/modules/services/mail/mlmmj.nix
+++ b/nixos/modules/services/mail/mlmmj.nix
@@ -16,7 +16,14 @@ let
   alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\"";
   subjectPrefix = list: "[${list}]";
   listAddress = domain: list: "${list}@${domain}";
-  customHeaders = domain: list: [ "List-Id: ${list}" "Reply-To: ${list}@${domain}" ];
+  customHeaders = domain: list: [
+    "List-Id: ${list}"
+    "Reply-To: ${list}@${domain}"
+    "List-Post: <mailto:${list}@${domain}>"
+    "List-Help: <mailto:${list}+help@${domain}>"
+    "List-Subscribe: <mailto:${list}+subscribe@${domain}>"
+    "List-Unsubscribe: <mailto:${list}+unsubscribe@${domain}>"
+  ];
   footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}";
   createList = d: l:
     let ctlDir = listCtl d l; in
@@ -110,17 +117,29 @@ in
     services.postfix = {
       enable = true;
       recipientDelimiter= "+";
-      extraMasterConf = ''
-        mlmmj unix - n n - - pipe flags=ORhu user=mlmmj argv=${pkgs.mlmmj}/bin/mlmmj-receive -F -L ${spoolDir}/$nexthop
-      '';
+      masterConfig.mlmmj = {
+        type = "unix";
+        private = true;
+        privileged = true;
+        chroot = false;
+        wakeup = 0;
+        command = "pipe";
+        args = [
+          "flags=ORhu"
+          "user=mlmmj"
+          "argv=${pkgs.mlmmj}/bin/mlmmj-receive"
+          "-F"
+          "-L"
+          "${spoolDir}/$nexthop"
+        ];
+      };
 
       extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists;
 
-      extraConfig = ''
-        transport_maps = hash:${stateDir}/transports
-        virtual_alias_maps = hash:${stateDir}/virtuals
-        propagate_unmatched_extensions = virtual
-      '';
+      extraConfig = "propagate_unmatched_extensions = virtual";
+
+      virtual = concatMapLines (virtual cfg.listDomain) cfg.mailLists;
+      transport = concatMapLines (transport cfg.listDomain) cfg.mailLists;
     };
 
     environment.systemPackages = [ pkgs.mlmmj ];
@@ -129,10 +148,8 @@ in
           ${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain}
           ${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir}
           ${concatMapLines (createList cfg.listDomain) cfg.mailLists}
-          echo "${concatMapLines (virtual cfg.listDomain) cfg.mailLists}" > ${stateDir}/virtuals
-          echo "${concatMapLines (transport cfg.listDomain) cfg.mailLists}" > ${stateDir}/transports
-          ${pkgs.postfix}/bin/postmap ${stateDir}/virtuals
-          ${pkgs.postfix}/bin/postmap ${stateDir}/transports
+          ${pkgs.postfix}/bin/postmap /etc/postfix/virtual
+          ${pkgs.postfix}/bin/postmap /etc/postfix/transport
       '';
 
     systemd.services.mlmmj-maintd = {
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index fd4d16cdc37b0..63c0961b7568b 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -25,8 +25,6 @@ let
 
   clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
 
-  smtpTlsSecurityLevel = if cfg.useDane then "dane" else "may";
-
   mainCf = let
     escape = replaceStrings ["$"] ["$$"];
     mkList = items: "\n  " + concatStringsSep ",\n  " items;
@@ -52,7 +50,7 @@ let
       };
 
       type = mkOption {
-        type = types.enum [ "inet" "unix" "fifo" "pass" ];
+        type = types.enum [ "inet" "unix" "unix-dgram" "fifo" "pass" ];
         default = "unix";
         example = "inet";
         description = "The type of the service";
@@ -510,14 +508,6 @@ in
         '';
       };
 
-      useDane = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Sets smtp_tls_security_level to "dane" rather than "may". See postconf(5) for details.
-        '';
-      };
-
       sslCert = mkOption {
         type = types.str;
         default = "";
@@ -570,6 +560,7 @@ in
 
       transport = mkOption {
         default = "";
+        type = types.lines;
         description = "
           Entries for the transport map, cf. man-page transport(8).
         ";
@@ -583,6 +574,7 @@ in
 
       dnsBlacklistOverrides = mkOption {
         default = "";
+        type = types.lines;
         description = "contents of check_client_access for overriding dnsBlacklists";
       };
 
@@ -819,13 +811,13 @@ in
       // optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; }
       // optionalAttrs (cfg.tlsTrustedAuthorities != "") {
         smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
-        smtp_tls_security_level = smtpTlsSecurityLevel;
+        smtp_tls_security_level = mkDefault "may";
       }
       // optionalAttrs (cfg.sslCert != "") {
         smtp_tls_cert_file = cfg.sslCert;
         smtp_tls_key_file = cfg.sslKey;
 
-        smtp_tls_security_level = smtpTlsSecurityLevel;
+        smtp_tls_security_level = mkDefault "may";
 
         smtpd_tls_cert_file = cfg.sslCert;
         smtpd_tls_key_file = cfg.sslKey;
@@ -834,12 +826,6 @@ in
       };
 
       services.postfix.masterConfig = {
-        smtp_inet = {
-          name = "smtp";
-          type = "inet";
-          private = false;
-          command = "smtpd";
-        };
         pickup = {
           private = false;
           wakeup = 60;
@@ -921,6 +907,12 @@ in
           in concatLists (mapAttrsToList mkKeyVal cfg.submissionOptions);
         };
       } // optionalAttrs cfg.enableSmtp {
+        smtp_inet = {
+          name = "smtp";
+          type = "inet";
+          private = false;
+          command = "smtpd";
+        };
         smtp = {};
         relay = {
           command = "smtp";
@@ -969,5 +961,9 @@ in
   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.")
+
+   (mkChangedOptionModule [ "services" "postfix" "useDane" ]
+     [ "services" "postfix" "config" "smtp_tls_security_level" ]
+     (config: mkIf config.services.postfix.useDane "dane"))
   ];
 }
diff --git a/nixos/modules/services/mail/postgrey.nix b/nixos/modules/services/mail/postgrey.nix
index 709f6b21aa0ad..7c206e3725e6b 100644
--- a/nixos/modules/services/mail/postgrey.nix
+++ b/nixos/modules/services/mail/postgrey.nix
@@ -163,7 +163,7 @@ in {
 
     systemd.services.postgrey = let
       bind-flag = if cfg.socket ? path then
-        ''--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}''
+        "--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}"
       else
         ''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}'';
     in {
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index a0bbab64985b2..ee7aa7e22fb96 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -204,6 +204,11 @@ in
     };
     systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
 
+    # Restart on config changes.
+    systemd.services.phpfpm-roundcube.restartTriggers = [
+      config.environment.etc."roundcube/config.inc.php".source
+    ];
+
     systemd.services.roundcube-setup = mkMerge [
       (mkIf (cfg.database.host == "localhost") {
         requires = [ "postgresql.service" ];
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index aacdbe2aeed25..2f9d28195bd83 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -153,7 +153,7 @@ let
 
       ${concatStringsSep "\n" (mapAttrsToList (name: value: let
           includeName = if name == "rspamd_proxy" then "proxy" else name;
-          tryOverride = if value.extraConfig == "" then "true" else "false";
+          tryOverride = boolToString (value.extraConfig == "");
         in ''
         worker "${value.type}" {
           type = "${value.type}";
@@ -371,6 +371,9 @@ in
     };
     services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
 
+    systemd.services.postfix.serviceConfig.SupplementaryGroups =
+      mkIf cfg.postfix.enable [ postfixCfg.group ];
+
     # Allow users to run 'rspamc' and 'rspamadm'.
     environment.systemPackages = [ pkgs.rspamd ];
 
@@ -394,21 +397,50 @@ in
       restartTriggers = [ rspamdDir ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c /etc/rspamd/rspamd.conf -f";
+        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
         Restart = "always";
+
+        User = "${cfg.user}";
+        Group = "${cfg.group}";
+        SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
+
         RuntimeDirectory = "rspamd";
+        RuntimeDirectoryMode = "0755";
+        StateDirectory = "rspamd";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = [];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
         PrivateTmp = true;
+        # we need to chown socket to rspamd-milter
+        PrivateUsers = !cfg.postfix.enable;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
       };
-
-      preStart = ''
-        ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd
-        ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd
-      '';
     };
   };
   imports = [
     (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
-	     "Socket activation never worked correctly and could at this time not be fixed and so was removed")
+       "Socket activation never worked correctly and could at this time not be fixed and so was removed")
     (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
     (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
     (mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service")
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
index 5e7c1e668288a..541f0d2db19f4 100644
--- a/nixos/modules/services/misc/autofs.nix
+++ b/nixos/modules/services/misc/autofs.nix
@@ -52,6 +52,7 @@ in
       };
 
       timeout = mkOption {
+        type = types.int;
         default = 600;
         description = "Set the global minimum timeout, in seconds, until directories are unmounted";
       };
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index cf7fb5f78d3d5..dfb418af6edeb 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -37,9 +37,9 @@ in {
       description = "Autorandr execution hook";
       after = [ "sleep.target" ];
 
+      startLimitIntervalSec = 5;
+      startLimitBurst = 1;
       serviceConfig = {
-        StartLimitInterval = 5;
-        StartLimitBurst = 1;
         ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
         Type = "oneshot";
         RemainAfterExit = false;
diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix
index dcf416022734f..15af1f50da1d6 100644
--- a/nixos/modules/services/misc/cfdyndns.nix
+++ b/nixos/modules/services/misc/cfdyndns.nix
@@ -6,6 +6,12 @@ let
   cfg = config.services.cfdyndns;
 in
 {
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "cfdyndns" "apikey" ]
+      "Use services.cfdyndns.apikeyFile instead.")
+  ];
+
   options = {
     services.cfdyndns = {
       enable = mkEnableOption "Cloudflare Dynamic DNS Client";
@@ -17,10 +23,12 @@ in
         '';
       };
 
-      apikey = mkOption {
-        type = types.str;
+      apikeyFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
         description = ''
-          The API Key to use to authenticate to CloudFlare.
+          The path to a file containing the API Key
+          used to authenticate with CloudFlare.
         '';
       };
 
@@ -45,13 +53,17 @@ in
         Type = "simple";
         User = config.ids.uids.cfdyndns;
         Group = config.ids.gids.cfdyndns;
-        ExecStart = "/bin/sh -c '${pkgs.cfdyndns}/bin/cfdyndns'";
       };
       environment = {
         CLOUDFLARE_EMAIL="${cfg.email}";
-        CLOUDFLARE_APIKEY="${cfg.apikey}";
         CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
       };
+      script = ''
+        ${optionalString (cfg.apikeyFile != null) ''
+          export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
+        ''}
+        ${pkgs.cfdyndns}/bin/cfdyndns
+      '';
     };
 
     users.users = {
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
index 7635c2a0f4e95..662570f9451fe 100644
--- a/nixos/modules/services/misc/cgminer.nix
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -41,12 +41,14 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "cgminer";
         description = "User account under which cgminer runs";
       };
 
       pools = mkOption {
         default = [];  # Run benchmark
+        type = types.listOf (types.attrsOf types.str);
         description = "List of pools where to mine";
         example = [{
           url = "http://p2pool.org:9332";
@@ -57,6 +59,7 @@ in
 
       hardware = mkOption {
         default = []; # Run without options
+        type = types.listOf (types.attrsOf (types.either types.str types.int));
         description= "List of config options for every GPU";
         example = [
         {
@@ -83,6 +86,7 @@ in
 
       config = mkOption {
         default = {};
+        type = (types.either types.bool types.int);
         description = "Additional config";
         example = {
           auto-fan = true;
@@ -120,18 +124,18 @@ in
       wantedBy = [ "multi-user.target" ];
 
       environment = {
-        LD_LIBRARY_PATH = ''/run/opengl-driver/lib:/run/opengl-driver-32/lib'';
+        LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib";
         DISPLAY = ":${toString config.services.xserver.display}";
         GPU_MAX_ALLOC_PERCENT = "100";
         GPU_USE_SYNC_OBJECTS = "1";
       };
 
+      startLimitIntervalSec = 60;  # 1 min
       serviceConfig = {
         ExecStart = "${pkgs.cgminer}/bin/cgminer --syslog --text-only --config ${cgminerConfig}";
         User = cfg.user;
         RestartSec = "30s";
         Restart = "always";
-        StartLimitInterval = "1m";
       };
     };
 
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
index d175854d2d1ef..6e796a3a1fcec 100644
--- a/nixos/modules/services/misc/dictd.nix
+++ b/nixos/modules/services/misc/dictd.nix
@@ -27,7 +27,7 @@ in
         default = with pkgs.dictdDBs; [ wiktionary wordnet ];
         defaultText = "with pkgs.dictdDBs; [ wiktionary wordnet ]";
         example = literalExample "[ pkgs.dictdDBs.nld2eng ]";
-        description = ''List of databases to make available.'';
+        description = "List of databases to make available.";
       };
 
     };
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
deleted file mode 100644
index 69386cdbb3812..0000000000000
--- a/nixos/modules/services/misc/disnix.nix
+++ /dev/null
@@ -1,88 +0,0 @@
-# Disnix server
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.disnix;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.disnix = {
-
-      enable = mkEnableOption "Disnix";
-
-      enableMultiUser = mkOption {
-        type = types.bool;
-        default = true;
-        description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
-      };
-
-      useWebServiceInterface = mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
-
-      package = mkOption {
-        type = types.path;
-        description = "The Disnix package";
-        default = pkgs.disnix;
-        defaultText = "pkgs.disnix";
-      };
-
-    };
-
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    dysnomia.enable = true;
-
-    environment.systemPackages = [ pkgs.disnix ] ++ optional cfg.useWebServiceInterface pkgs.DisnixWebService;
-
-    services.dbus.enable = true;
-    services.dbus.packages = [ pkgs.disnix ];
-
-    services.tomcat.enable = cfg.useWebServiceInterface;
-    services.tomcat.extraGroups = [ "disnix" ];
-    services.tomcat.javaOpts = "${optionalString cfg.useWebServiceInterface "-Djava.library.path=${pkgs.libmatthew_java}/lib/jni"} ";
-    services.tomcat.sharedLibs = optional cfg.useWebServiceInterface "${pkgs.DisnixWebService}/share/java/DisnixConnection.jar"
-      ++ optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
-    services.tomcat.webapps = optional cfg.useWebServiceInterface pkgs.DisnixWebService;
-
-    users.groups.disnix.gid = config.ids.gids.disnix;
-
-    systemd.services = {
-      disnix = mkIf cfg.enableMultiUser {
-        description = "Disnix server";
-        wants = [ "dysnomia.target" ];
-        wantedBy = [ "multi-user.target" ];
-        after = [ "dbus.service" ]
-          ++ optional config.services.httpd.enable "httpd.service"
-          ++ optional config.services.mysql.enable "mysql.service"
-          ++ optional config.services.postgresql.enable "postgresql.service"
-          ++ optional config.services.tomcat.enable "tomcat.service"
-          ++ optional config.services.svnserve.enable "svnserve.service"
-          ++ optional config.services.mongodb.enable "mongodb.service";
-
-        restartIfChanged = false;
-
-        path = [ config.nix.package cfg.package config.dysnomia.package "/run/current-system/sw" ];
-
-        environment = {
-          HOME = "/root";
-        }
-        // (if config.environment.variables ? DYSNOMIA_CONTAINERS_PATH then { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; } else {})
-        // (if config.environment.variables ? DYSNOMIA_MODULES_PATH then { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; } else {});
-
-        serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
-      };
-
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
deleted file mode 100644
index 4b52963500d1b..0000000000000
--- a/nixos/modules/services/misc/dysnomia.nix
+++ /dev/null
@@ -1,217 +0,0 @@
-{pkgs, lib, config, ...}:
-
-with lib;
-
-let
-  cfg = config.dysnomia;
-
-  printProperties = properties:
-    concatMapStrings (propertyName:
-      let
-        property = properties.${propertyName};
-      in
-      if isList property then "${propertyName}=(${lib.concatMapStrings (elem: "\"${toString elem}\" ") (properties.${propertyName})})\n"
-      else "${propertyName}=\"${toString property}\"\n"
-    ) (builtins.attrNames properties);
-
-  properties = pkgs.stdenv.mkDerivation {
-    name = "dysnomia-properties";
-    buildCommand = ''
-      cat > $out << "EOF"
-      ${printProperties cfg.properties}
-      EOF
-    '';
-  };
-
-  containersDir = pkgs.stdenv.mkDerivation {
-    name = "dysnomia-containers";
-    buildCommand = ''
-      mkdir -p $out
-      cd $out
-
-      ${concatMapStrings (containerName:
-        let
-          containerProperties = cfg.containers.${containerName};
-        in
-        ''
-          cat > ${containerName} <<EOF
-          ${printProperties containerProperties}
-          type=${containerName}
-          EOF
-        ''
-      ) (builtins.attrNames cfg.containers)}
-    '';
-  };
-
-  linkMutableComponents = {containerName}:
-    ''
-      mkdir ${containerName}
-
-      ${concatMapStrings (componentName:
-        let
-          component = cfg.components.${containerName}.${componentName};
-        in
-        "ln -s ${component} ${containerName}/${componentName}\n"
-      ) (builtins.attrNames (cfg.components.${containerName} or {}))}
-    '';
-
-  componentsDir = pkgs.stdenv.mkDerivation {
-    name = "dysnomia-components";
-    buildCommand = ''
-      mkdir -p $out
-      cd $out
-
-      ${concatMapStrings (containerName:
-        linkMutableComponents { inherit containerName; }
-      ) (builtins.attrNames cfg.components)}
-    '';
-  };
-in
-{
-  options = {
-    dysnomia = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable Dysnomia";
-      };
-
-      enableAuthentication = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to publish privacy-sensitive authentication credentials";
-      };
-
-      package = mkOption {
-        type = types.path;
-        description = "The Dysnomia package";
-      };
-
-      properties = mkOption {
-        description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
-        default = {};
-      };
-
-      containers = mkOption {
-        description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
-        default = {};
-      };
-
-      components = mkOption {
-        description = "An atttribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
-        default = {};
-      };
-
-      extraContainerProperties = mkOption {
-        description = "An attribute set providing additional container settings in addition to the default properties";
-        default = {};
-      };
-
-      extraContainerPaths = mkOption {
-        description = "A list of paths containing additional container configurations that are added to the search folders";
-        default = [];
-      };
-
-      extraModulePaths = mkOption {
-        description = "A list of paths containing additional modules that are added to the search folders";
-        default = [];
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-
-    environment.etc = {
-      "dysnomia/containers" = {
-        source = containersDir;
-      };
-      "dysnomia/components" = {
-        source = componentsDir;
-      };
-      "dysnomia/properties" = {
-        source = properties;
-      };
-    };
-
-    environment.variables = {
-      DYSNOMIA_STATEDIR = "/var/state/dysnomia-nixos";
-      DYSNOMIA_CONTAINERS_PATH = "${lib.concatMapStrings (containerPath: "${containerPath}:") cfg.extraContainerPaths}/etc/dysnomia/containers";
-      DYSNOMIA_MODULES_PATH = "${lib.concatMapStrings (modulePath: "${modulePath}:") cfg.extraModulePaths}/etc/dysnomia/modules";
-    };
-
-    environment.systemPackages = [ cfg.package ];
-
-    dysnomia.package = pkgs.dysnomia.override (origArgs: {
-      enableApacheWebApplication = config.services.httpd.enable;
-      enableAxis2WebService = config.services.tomcat.axis2.enable;
-      enableEjabberdDump = config.services.ejabberd.enable;
-      enableMySQLDatabase = config.services.mysql.enable;
-      enablePostgreSQLDatabase = config.services.postgresql.enable;
-      enableSubversionRepository = config.services.svnserve.enable;
-      enableTomcatWebApplication = config.services.tomcat.enable;
-      enableMongoDatabase = config.services.mongodb.enable;
-      enableInfluxDatabase = config.services.influxdb.enable;
-    });
-
-    dysnomia.properties = {
-      hostname = config.networking.hostName;
-      inherit (config.nixpkgs.localSystem) system;
-
-      supportedTypes = (import "${pkgs.stdenv.mkDerivation {
-        name = "supportedtypes";
-        buildCommand = ''
-          ( echo -n "[ "
-            cd ${cfg.package}/libexec/dysnomia
-            for i in *
-            do
-                echo -n "\"$i\" "
-            done
-            echo -n " ]") > $out
-        '';
-      }}");
-    };
-
-    dysnomia.containers = lib.recursiveUpdate ({
-      process = {};
-      wrapper = {};
-    }
-    // lib.optionalAttrs (config.services.httpd.enable) { apache-webapplication = {
-      documentRoot = config.services.httpd.virtualHosts.localhost.documentRoot;
-    }; }
-    // lib.optionalAttrs (config.services.tomcat.axis2.enable) { axis2-webservice = {}; }
-    // lib.optionalAttrs (config.services.ejabberd.enable) { ejabberd-dump = {
-      ejabberdUser = config.services.ejabberd.user;
-    }; }
-    // lib.optionalAttrs (config.services.mysql.enable) { mysql-database = {
-        mysqlPort = config.services.mysql.port;
-      } // lib.optionalAttrs cfg.enableAuthentication {
-        mysqlUsername = "root";
-        mysqlPassword = builtins.readFile (config.services.mysql.rootPassword);
-      };
-    }
-    // lib.optionalAttrs (config.services.postgresql.enable) { postgresql-database = {
-      } // lib.optionalAttrs (cfg.enableAuthentication) {
-        postgresqlUsername = "postgres";
-      };
-    }
-    // lib.optionalAttrs (config.services.tomcat.enable) { tomcat-webapplication = {
-      tomcatPort = 8080;
-    }; }
-    // lib.optionalAttrs (config.services.mongodb.enable) { mongo-database = {}; }
-    // lib.optionalAttrs (config.services.svnserve.enable) { subversion-repository = {
-      svnBaseDir = config.services.svnserve.svnBaseDir;
-    }; }) cfg.extraContainerProperties;
-
-    system.activationScripts.dysnomia = ''
-      mkdir -p /etc/systemd-mutable/system
-      if [ ! -f /etc/systemd-mutable/system/dysnomia.target ]
-      then
-          ( echo "[Unit]"
-            echo "Description=Services that are activated and deactivated by Dysnomia"
-            echo "After=final.target"
-          ) > /etc/systemd-mutable/system/dysnomia.target
-      fi
-    '';
-  };
-}
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
new file mode 100644
index 0000000000000..d9d12698d79dc
--- /dev/null
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -0,0 +1,205 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etebase-server;
+
+  pythonEnv = pkgs.python3.withPackages (ps: with ps;
+    [ etebase-server daphne ]);
+
+  dbConfig = {
+    sqlite3 = ''
+      engine = django.db.backends.sqlite3
+      name = ${cfg.dataDir}/db.sqlite3
+    '';
+  };
+
+  defaultConfigIni = toString (pkgs.writeText "etebase-server.ini" ''
+    [global]
+    debug = false
+    secret_file = ${if cfg.secretFile != null then cfg.secretFile else ""}
+    media_root = ${cfg.dataDir}/media
+
+    [allowed_hosts]
+    allowed_host1 = ${cfg.host}
+
+    [database]
+    ${dbConfig."${cfg.database.type}"}
+  '');
+
+  configIni = if cfg.customIni != null then cfg.customIni else defaultConfigIni;
+
+  defaultUser = "etebase-server";
+in
+{
+  options = {
+    services.etebase-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable the Etebase server.
+
+          Once enabled you need to create an admin user using the
+          shell command <literal>etebase-server createsuperuser</literal>.
+          Then you can login and create accounts on your-etebase-server.com/admin
+        '';
+      };
+
+      secretFile = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+          The path to a file containing the secret
+          used as django's SECRET_KEY.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/etebase-server";
+        description = "Directory to store the Etebase server data.";
+      };
+
+      port = mkOption {
+        type = with types; nullOr port;
+        default = 8001;
+        description = "Port to listen on.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        example = "localhost";
+        description = ''
+          Host to listen on.
+        '';
+      };
+
+      unixSocket = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "The path to the socket to bind to.";
+        example = "/run/etebase-server/etebase-server.sock";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" ];
+          default = "sqlite3";
+          description = ''
+            Database engine to use.
+            Currently only sqlite3 is supported.
+            Other options can be configured using <literal>extraConfig</literal>.
+          '';
+        };
+      };
+
+      customIni = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Custom etebase-server.ini.
+
+          See <literal>etebase-src/etebase-server.ini.example</literal> for available options.
+
+          Setting this option overrides the default config which is generated from the options
+          <literal>secretFile</literal>, <literal>host</literal> and <literal>database</literal>.
+        '';
+        example = literalExample ''
+          [global]
+          debug = false
+          secret_file = /path/to/secret
+          media_root = /path/to/media
+
+          [allowed_hosts]
+          allowed_host1 = example.com
+
+          [database]
+          engine = django.db.backends.sqlite3
+          name = db.sqlite3
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = "User under which Etebase server runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs; [
+      (runCommand "etebase-server" {
+        buildInputs = [ makeWrapper ];
+      } ''
+        makeWrapper ${pythonEnv}/bin/etebase-server \
+          $out/bin/etebase-server \
+          --run "cd ${cfg.dataDir}" \
+          --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
+      '')
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+    ];
+
+    systemd.services.etebase-server = {
+      description = "An Etebase (EteSync 2.0) server";
+      after = [ "network.target" "systemd-tmpfiles-setup.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "always";
+        WorkingDirectory = cfg.dataDir;
+      };
+      environment = {
+        PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}";
+        ETEBASE_EASY_CONFIG_PATH="${configIni}";
+      };
+      preStart = ''
+        # Auto-migrate on first run or if the package has changed
+        versionFile="${cfg.dataDir}/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
+          ${pythonEnv}/bin/etebase-server migrate
+          echo ${pkgs.etebase-server} > "$versionFile"
+        fi
+      '';
+      script =
+        let
+          networking = if cfg.unixSocket != null
+          then "-u ${cfg.unixSocket}"
+          else "-b 0.0.0.0 -p ${toString cfg.port}";
+        in ''
+          cd "${pythonEnv}/lib/etebase-server";
+          ${pythonEnv}/bin/daphne ${networking} \
+            etebase_server.asgi:application
+        '';
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        group = defaultUser;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/etesync-dav.nix b/nixos/modules/services/misc/etesync-dav.nix
new file mode 100644
index 0000000000000..9d7cfda371b13
--- /dev/null
+++ b/nixos/modules/services/misc/etesync-dav.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etesync-dav;
+in
+  {
+    options.services.etesync-dav = {
+      enable = mkEnableOption "etesync-dav";
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The server host address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 37358;
+        description = "The server host port.";
+      };
+
+      apiUrl = mkOption {
+        type = types.str;
+        default = "https://api.etesync.com/";
+        description = "The url to the etesync API.";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+
+      sslCertificate = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.crt";
+        description = ''
+          Path to server SSL certificate. It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+
+      sslCertificateKey = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.key";
+        description = ''
+          Path to server SSL certificate key.  It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      systemd.services.etesync-dav = {
+        description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.etesync-dav ];
+        environment = {
+          ETESYNC_LISTEN_ADDRESS = cfg.host;
+          ETESYNC_LISTEN_PORT = toString cfg.port;
+          ETESYNC_URL = cfg.apiUrl;
+          ETESYNC_DATA_DIR = "/var/lib/etesync-dav";
+        };
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = "etesync-dav";
+          ExecStart = "${pkgs.etesync-dav}/bin/etesync-dav";
+          ExecStartPre = mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
+            pkgs.writers.writeBash "etesync-dav-copy-keys" ''
+              ${optionalString (cfg.sslCertificate != null) ''
+                cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
+              ''}
+              ${optionalString (cfg.sslCertificateKey != null) ''
+                cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
+              ''}
+            ''
+          );
+          Restart = "on-failure";
+          RestartSec = "30min 1s";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index f8c79f892da36..28c98edf47afe 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -185,7 +185,7 @@ in
       };
       zkExtraCfg = mkOption {
         type = types.str;
-        default = ''initLimit=5&syncLimit=2&tickTime=2000'';
+        default = "initLimit=5&syncLimit=2&tickTime=2000";
         description = ''
           Extra options to pass into Zookeeper
         '';
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
index 21740c8c0b725..8d438bb9eb197 100644
--- a/nixos/modules/services/misc/felix.nix
+++ b/nixos/modules/services/misc/felix.nix
@@ -27,11 +27,13 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "osgi";
         description = "User account under which Apache Felix runs.";
       };
 
       group = mkOption {
+        type = types.str;
         default = "osgi";
         description = "Group account under which Apache Felix runs.";
       };
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
index b8841a7fe74ce..5258f5acb410c 100644
--- a/nixos/modules/services/misc/fstrim.nix
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -31,7 +31,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    systemd.packages = [ pkgs.utillinux ];
+    systemd.packages = [ pkgs.util-linux ];
 
     systemd.timers.fstrim = {
       timerConfig = {
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index af80e99746bef..434e2d2429b5b 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -349,7 +349,7 @@ in
         {
           DOMAIN = cfg.domain;
           STATIC_ROOT_PATH = cfg.staticRootPath;
-          LFS_JWT_SECRET = "#jwtsecret#";
+          LFS_JWT_SECRET = "#lfsjwtsecret#";
           ROOT_URL = cfg.rootUrl;
         }
         (mkIf cfg.enableUnixSocket {
@@ -381,6 +381,7 @@ in
 
       security = {
         SECRET_KEY = "#secretkey#";
+        INTERNAL_TOKEN = "#internaltoken#";
         INSTALL_LOCK = true;
       };
 
@@ -396,6 +397,10 @@ in
       mailer = mkIf (cfg.mailerPasswordFile != null) {
         PASSWD = "#mailerpass#";
       };
+
+      oauth2 = {
+        JWT_SECRET = "#oauth2jwtsecret#";
+      };
     };
 
     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
@@ -453,12 +458,22 @@ in
       description = "gitea";
       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ gitea pkgs.gitAndTools.git ];
-
+      path = [ gitea pkgs.git ];
+
+      # In older versions the secret naming for JWT was kind of confusing.
+      # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
+      # wasn't persistant at all.
+      # To fix that, there is now the file oauth2_jwt_secret containing the
+      # values for JWT_SECRET and the file jwt_secret gets renamed to
+      # lfs_jwt_secret.
+      # We have to consider this to stay compatible with older installations.
       preStart = let
         runConfig = "${cfg.stateDir}/custom/conf/app.ini";
         secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        jwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret";
+        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
       in ''
         # copy custom configuration and generate a random secret key if needed
         ${optionalString (cfg.useWizard == false) ''
@@ -468,24 +483,41 @@ in
               ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
           fi
 
-          if [ ! -e ${jwtSecret} ]; then
-              ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${jwtSecret}
+          # Migrate LFS_JWT_SECRET filename
+          if [[ -e ${oldLfsJwtSecret} && ! -e ${lfsJwtSecret} ]]; then
+              mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+          fi
+
+          if [ ! -e ${oauth2JwtSecret} ]; then
+              ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+          fi
+
+          if [ ! -e ${lfsJwtSecret} ]; then
+              ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+          fi
+
+          if [ ! -e ${internalToken} ]; then
+              ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
           fi
 
-          KEY="$(head -n1 ${secretKey})"
+          SECRETKEY="$(head -n1 ${secretKey})"
           DBPASS="$(head -n1 ${cfg.database.passwordFile})"
-          JWTSECRET="$(head -n1 ${jwtSecret})"
+          OAUTH2JWTSECRET="$(head -n1 ${oauth2JwtSecret})"
+          LFSJWTSECRET="$(head -n1 ${lfsJwtSecret})"
+          INTERNALTOKEN="$(head -n1 ${internalToken})"
           ${if (cfg.mailerPasswordFile == null) then ''
             MAILERPASSWORD="#mailerpass#"
           '' else ''
             MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
           ''}
-          sed -e "s,#secretkey#,$KEY,g" \
+          sed -e "s,#secretkey#,$SECRETKEY,g" \
               -e "s,#dbpass#,$DBPASS,g" \
-              -e "s,#jwtsecret#,$JWTSECRET,g" \
+              -e "s,#oauth2jwtsecret#,$OAUTH2JWTSECRET,g" \
+              -e "s,#lfsjwtsecret#,$LFSJWTSECRET,g" \
+              -e "s,#internaltoken#,$INTERNALTOKEN,g" \
               -e "s,#mailerpass#,$MAILERPASSWORD,g" \
               -i ${runConfig}
-          chmod 640 ${runConfig} ${secretKey} ${jwtSecret}
+          chmod 640 ${runConfig} ${secretKey} ${oauth2JwtSecret} ${lfsJwtSecret} ${internalToken}
         ''}
 
         # update all hooks' binary paths
@@ -565,8 +597,7 @@ in
     users.groups.gitea = {};
 
     warnings =
-      optional (cfg.database.password != "") ''
-        config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead.'' ++
+      optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
       optional (cfg.extraConfig != null) ''
         services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
       '';
@@ -605,5 +636,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
-  meta.maintainers = with lib.maintainers; [ srhb ];
+  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
 }
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
index 1ec030549f98e..f09565283f3c1 100644
--- a/nixos/modules/services/misc/gitit.nix
+++ b/nixos/modules/services/misc/gitit.nix
@@ -42,6 +42,7 @@ let
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 9896b8023e441..61faeab7d3212 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -43,9 +43,16 @@ let
 
     [gitlab-shell]
     dir = "${cfg.packages.gitlab-shell}"
+
+    [hooks]
+    custom_hooks_dir = "${cfg.statePath}/custom_hooks"
+
+    [gitlab]
     secret_file = "${cfg.statePath}/gitlab_shell_secret"
-    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"
-    http_settings = { self_signed_cert = false }
+    url = "http+unix://${pathUrlQuote gitlabSocket}"
+
+    [gitlab.http-settings]
+    self_signed_cert = false
 
     ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
     [[storage]]
@@ -61,7 +68,6 @@ let
     repos_path = "${cfg.statePath}/repositories";
     secret_file = "${cfg.statePath}/gitlab_shell_secret";
     log_file = "${cfg.statePath}/log/gitlab-shell.log";
-    custom_hooks_dir = "${cfg.statePath}/custom_hooks";
     redis = {
       bin = "${pkgs.redis}/bin/redis-cli";
       host = "127.0.0.1";
@@ -73,6 +79,11 @@ let
 
   redisConfig.production.url = cfg.redisUrl;
 
+  pagesArgs = [
+    "-pages-domain" gitlabConfig.production.pages.host
+    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
+  ] ++ cfg.pagesExtraArgs;
+
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
     production = flip recursiveUpdate cfg.extraConfig {
@@ -114,6 +125,7 @@ let
         receive_pack = true;
       };
       workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
+      gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
       git.bin_path = "git";
       monitoring = {
         ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
@@ -236,6 +248,13 @@ in {
         description = "Reference to the gitaly package";
       };
 
+      packages.pages = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-pages;
+        defaultText = "pkgs.gitlab-pages";
+        description = "Reference to the gitlab-pages package";
+      };
+
       statePath = mkOption {
         type = types.str;
         default = "/var/gitlab/state";
@@ -435,7 +454,7 @@ in {
         authentication = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+          description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
         };
 
         enableStartTLSAuto = mkOption {
@@ -451,6 +470,12 @@ in {
         };
       };
 
+      pagesExtraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "-listen-proxy" "127.0.0.1:8090" ];
+        description = "Arguments to pass to the gitlab-pages daemon";
+      };
+
       secrets.secretFile = mkOption {
         type = with types; nullOr path;
         default = null;
@@ -635,7 +660,7 @@ in {
       script = ''
         set -eu
 
-        PSQL="${pkgs.utillinux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}"
+        PSQL="${pkgs.util-linux}/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}'")
@@ -650,6 +675,7 @@ in {
             rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
         fi
         $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
       '';
 
       serviceConfig = {
@@ -710,7 +736,7 @@ in {
       environment = gitlabEnv;
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         ruby
         openssh
         nodejs
@@ -732,12 +758,13 @@ in {
     };
 
     systemd.services.gitaly = {
-      after = [ "network.target" ];
+      after = [ "network.target" "gitlab.service" ];
+      bindsTo = [ "gitlab.service" ];
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [
         openssh
         procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
-        gitAndTools.git
+        git
         cfg.packages.gitaly.rubyEnv
         cfg.packages.gitaly.rubyEnv.wrappedRuby
         gzip
@@ -754,12 +781,32 @@ in {
       };
     };
 
+    systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
+      description = "GitLab static pages daemon";
+      after = [ "network.target" "redis.service" "gitlab.service" ]; # gitlab.service creates configs
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.unzip ];
+
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+
+        ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [
         exiftool
-        gitAndTools.git
+        git
         gnutar
         gzip
         openssh
@@ -801,13 +848,13 @@ in {
     };
 
     systemd.services.gitlab = {
-      after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "gitlab-postgresql.service" "redis.service" ];
+      after = [ "gitlab-workhorse.service" "network.target" "gitlab-postgresql.service" "redis.service" ];
       requires = [ "gitlab-sidekiq.service" ];
       wantedBy = [ "multi-user.target" ];
       environment = gitlabEnv;
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         openssh
         nodejs
         procps
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
index 59cbdac319c8b..190ea9212d2aa 100644
--- a/nixos/modules/services/misc/gitolite.nix
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -227,6 +227,6 @@ in
     };
 
     environment.systemPackages = [ pkgs.gitolite pkgs.git ]
-        ++ optional cfg.enableGitAnnex pkgs.gitAndTools.git-annex;
+        ++ optional cfg.enableGitAnnex pkgs.git-annex;
   });
 }
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
index c5070aaa356a0..d7233f10c7cb8 100644
--- a/nixos/modules/services/misc/gogs.nix
+++ b/nixos/modules/services/misc/gogs.nix
@@ -25,7 +25,6 @@ let
     HTTP_ADDR = ${cfg.httpAddress}
     HTTP_PORT = ${toString cfg.httpPort}
     ROOT_URL = ${cfg.rootUrl}
-    STATIC_ROOT_PATH = ${cfg.staticRootPath}
 
     [session]
     COOKIE_NAME = session
@@ -179,13 +178,6 @@ in
         '';
       };
 
-      staticRootPath = mkOption {
-        type = types.str;
-        default = "${pkgs.gogs.data}";
-        example = "/var/lib/gogs/data";
-        description = "Upper level of template and static files path.";
-      };
-
       extraConfig = mkOption {
         type = types.str;
         default = "";
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 0477254e7c18d..1f2e13f373257 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -245,7 +245,11 @@ in {
         Group = "hass";
         Restart = "on-failure";
         ProtectSystem = "strict";
-        ReadWritePaths = "${cfg.configDir}";
+        ReadWritePaths = let
+          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
+          value = attrByPath cfgPath [] cfg;
+          allowPaths = if isList value then value else singleton value;
+        in [ "${cfg.configDir}" ] ++ allowPaths;
         KillSignal = "SIGINT";
         PrivateTmp = true;
         RemoveIPC = true;
diff --git a/nixos/modules/services/misc/ihaskell.nix b/nixos/modules/services/misc/ihaskell.nix
index 684a242d7385b..c7332b87803a9 100644
--- a/nixos/modules/services/misc/ihaskell.nix
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -21,6 +21,7 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index 0493dadea94ea..6a47dc3628f4a 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -45,6 +45,46 @@ in
         CacheDirectory = "jellyfin";
         ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
         Restart = "on-failure";
+
+        # Security options:
+
+        NoNewPrivileges = true;
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RemoveIPC = true;
+
+        RestrictNamespaces = true;
+        # AF_NETLINK needed because Jellyfin monitors the network connection
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+
+          "~@chown" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@module"
+          "~@obsolete" "~@privileged" "~@setuid"
+        ];
       };
     };
 
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
new file mode 100644
index 0000000000000..2f04c011a650f
--- /dev/null
+++ b/nixos/modules/services/misc/klipper.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.klipper;
+  package = pkgs.klipper;
+  format = pkgs.formats.ini { mkKeyValue = generators.mkKeyValueDefault {} ":"; };
+in
+{
+  ##### interface
+  options = {
+    services.klipper = {
+      enable = mkEnableOption "Klipper, the 3D printer firmware";
+
+      octoprintIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allows Octoprint to control Klipper.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
+      message = "Option klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
+    }];
+
+    environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;
+
+    systemd.services.klipper = {
+      description = "Klipper 3D Printer Firmware";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${package}/lib/klipper/klippy.py --input-tty=/run/klipper/tty /etc/klipper.cfg";
+        RuntimeDirectory = "klipper";
+        SupplementaryGroups = [ "dialout" ];
+        WorkingDirectory = "${package}/lib";
+      } // (if cfg.octoprintIntegration then {
+        Group = config.services.octoprint.group;
+        User = config.services.octoprint.user;
+      } else {
+        DynamicUser = true;
+        User = "klipper";
+      });
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
index 49c41ff637a8f..71d1227f4ff72 100644
--- a/nixos/modules/services/misc/matrix-appservice-discord.nix
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -5,7 +5,7 @@ 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";
+  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   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);
@@ -22,12 +22,6 @@ in {
         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
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 7f42184735c89..8e3fa60206c2d 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -504,8 +504,7 @@ in {
       report_stats = mkOption {
         type = types.bool;
         default = false;
-        description = ''
-        '';
+        description = "";
       };
       servers = mkOption {
         type = types.attrsOf (types.attrsOf types.str);
@@ -713,7 +712,7 @@ in {
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
-        ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
index fbfa838b168bf..358b631eb485b 100644
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -69,6 +69,9 @@ in {
       # i.e. to delegate from the host being accessible as ${config.networking.domain}
       # to another host actually running the Matrix homeserver.
       "${config.networking.domain}" = {
+        <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_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> =
           let
             # use 443 instead of the default 8448 port to unite
@@ -203,7 +206,7 @@ Success!
     <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}";
+          "base_url" = "https://${fqdn}";
           "server_name" = "${fqdn}";
         };
       };
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
index c5e8a5b85ec2d..caeb4b04164f0 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -21,6 +21,7 @@ in {
         default = {
           appservice = rec {
             database = "sqlite:///${dataDir}/mautrix-telegram.db";
+            database_opts = {};
             hostname = "0.0.0.0";
             port = 8080;
             address = "http://localhost:${toString port}";
@@ -29,6 +30,8 @@ in {
           bridge = {
             permissions."*" = "relaybot";
             relaybot.whitelist = [ ];
+            double_puppet_server_map = {};
+            login_shared_secret_map = {};
           };
 
           logging = {
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
new file mode 100644
index 0000000000000..516d0f70ef0b8
--- /dev/null
+++ b/nixos/modules/services/misc/n8n.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.n8n;
+  format = pkgs.formats.json {};
+  configFile = format.generate "n8n.json" cfg.settings;
+in
+{
+  options.services.n8n = {
+
+    enable = mkEnableOption "n8n server";
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open ports in the firewall for the n8n web interface.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Configuration for n8n, see <link xlink:href="https://docs.n8n.io/reference/configuration.html"/>
+        for supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    services.n8n.settings = {
+      # We use this to open the firewall, so we need to know about the default at eval time
+      port = lib.mkDefault 5678;
+    };
+
+    systemd.services.n8n = {
+      description = "N8N service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        # This folder must be writeable as the application is storing
+        # its data in it, so the StateDirectory is a good choice
+        N8N_USER_FOLDER = "/var/lib/n8n";
+        N8N_CONFIG_FILES = "${configFile}";
+      };
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.n8n}/bin/n8n";
+        Restart = "on-failure";
+        StateDirectory = "n8n";
+
+        # Basic Hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        DynamicUser = "true";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 2680b1cc0d3b8..64bdbf159d511 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -45,7 +45,7 @@ let
         trusted-substituters = ${toString cfg.trustedBinaryCaches}
         trusted-public-keys = ${toString cfg.binaryCachePublicKeys}
         auto-optimise-store = ${boolToString cfg.autoOptimiseStore}
-        require-sigs = ${if cfg.requireSignedBinaryCaches then "true" else "false"}
+        require-sigs = ${boolToString cfg.requireSignedBinaryCaches}
         trusted-users = ${toString cfg.trustedUsers}
         allowed-users = ${toString cfg.allowedUsers}
         ${optionalString (!cfg.distributedBuilds) ''
@@ -539,7 +539,7 @@ in
     systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
 
     systemd.services.nix-daemon =
-      { path = [ nix pkgs.utillinux config.programs.ssh.package ]
+      { path = [ nix pkgs.util-linux config.programs.ssh.package ]
           ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
 
         environment = cfg.envVars
@@ -587,10 +587,10 @@ in
 
     nix.systemFeatures = mkDefault (
       [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
-      optionals (pkgs.hostPlatform.platform ? gcc.arch) (
-        # a builder can run code for `platform.gcc.arch` and inferior architectures
-        [ "gccarch-${pkgs.hostPlatform.platform.gcc.arch}" ] ++
-        map (x: "gccarch-${x}") lib.systems.architectures.inferiors.${pkgs.hostPlatform.platform.gcc.arch}
+      optionals (pkgs.hostPlatform ? gcc.arch) (
+        # a builder can run code for `gcc.arch` and inferior architectures
+        [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
+        map (x: "gccarch-${x}") lib.systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
       )
     );
 
diff --git a/nixos/modules/services/misc/nzbhydra2.nix b/nixos/modules/services/misc/nzbhydra2.nix
new file mode 100644
index 0000000000000..c396b4b8f6e94
--- /dev/null
+++ b/nixos/modules/services/misc/nzbhydra2.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.nzbhydra2;
+
+in {
+  options = {
+    services.nzbhydra2 = {
+      enable = mkEnableOption "NZBHydra2";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/nzbhydra2";
+        description = "The directory where NZBHydra2 stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Open ports in the firewall for the NZBHydra2 web interface.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nzbhydra2;
+        defaultText = "pkgs.nzbhydra2";
+        description = "NZBHydra2 package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 0700 nzbhydra2 nzbhydra2 - -" ];
+
+    systemd.services.nzbhydra2 = {
+      description = "NZBHydra2";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "nzbhydra2";
+        Group = "nzbhydra2";
+        ExecStart =
+          "${cfg.package}/bin/nzbhydra2 --nobrowser --datafolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+        # Hardening
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        ProtectHome = "read-only";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies ="AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
+
+    users.users.nzbhydra2 = {
+      group = "nzbhydra2";
+      isSystemUser = true;
+    };
+
+    users.groups.nzbhydra2 = {};
+  };
+}
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index e2fbd3b401cc5..5a64946f9f63d 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -66,9 +66,10 @@ in
       };
 
       plugins = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = plugins: [];
         defaultText = "plugins: []";
-        example = literalExample "plugins: with plugins; [ m33-fio stlviewer ]";
+        example = literalExample "plugins: with plugins; [ themeify stlviewer ]";
         description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
       };
 
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index d6aeae48ccb62..2f752bcc7ed6b 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
   cfg = config.services.pykms;
   libDir = "/var/lib/pykms";
 
-in {
+in
+{
   meta.maintainers = with lib.maintainers; [ peterhoeg ];
 
   imports = [
@@ -46,14 +46,14 @@ in {
       };
 
       logLevel = mkOption {
-        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MINI" ];
+        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
         default = "INFO";
         description = "How much to log";
       };
 
       extraArgs = mkOption {
         type = types.listOf types.str;
-        default = [];
+        default = [ ];
         description = "Additional arguments";
       };
     };
@@ -74,8 +74,9 @@ in {
         ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
         ExecStart = lib.concatStringsSep " " ([
           "${getBin pykms}/bin/server"
-          "--logfile STDOUT"
-          "--loglevel ${cfg.logLevel}"
+          "--logfile=STDOUT"
+          "--loglevel=${cfg.logLevel}"
+          "--sqlite=${libDir}/clients.db"
         ] ++ cfg.extraArgs ++ [
           cfg.listenAddress
           (toString cfg.port)
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 1313bdaccc49a..8b53eb471db69 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -230,7 +230,7 @@ in
       production = {
         scm_subversion_command = "${pkgs.subversion}/bin/svn";
         scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
-        scm_git_command = "${pkgs.gitAndTools.git}/bin/git";
+        scm_git_command = "${pkgs.git}/bin/git";
         scm_cvs_command = "${pkgs.cvs}/bin/cvs";
         scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
         scm_darcs_command = "${pkgs.darcs}/bin/darcs";
@@ -299,7 +299,7 @@ in
         breezy
         cvs
         darcs
-        gitAndTools.git
+        git
         mercurial
         subversion
       ];
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index ef34e3a779f01..2fce3b9dc94c7 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -389,6 +389,7 @@ in
 
       extraConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra lines to be added verbatim to the rippled.cfg configuration file.
         '';
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
index 6ecb0d13187c1..1e748195e41aa 100644
--- a/nixos/modules/services/misc/safeeyes.nix
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -32,14 +32,14 @@ in
       wantedBy = [ "graphical-session.target" ];
       partOf   = [ "graphical-session.target" ];
 
+      startLimitIntervalSec = 350;
+      startLimitBurst = 10;
       serviceConfig = {
         ExecStart = ''
           ${pkgs.safeeyes}/bin/safeeyes
         '';
         Restart = "on-failure";
         RestartSec = 3;
-        StartLimitInterval = 350;
-        StartLimitBurst = 10;
       };
     };
 
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index 0e87fc461d3fc..20fe0793b84b6 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -39,7 +39,7 @@ in
         default = false;
         description = ''
           Whether to enable the Siproxd SIP
-	  proxy/masquerading daemon.
+          proxy/masquerading daemon.
         '';
       };
 
@@ -57,29 +57,29 @@ in
 
       hostsAllowReg = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "192.168.1.0/24" "192.168.2.0/24" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP registrations.
         '';
       };
 
       hostsAllowSip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "123.45.0.0/16" "123.46.0.0/16" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP traffic.
         '';
       };
 
       hostsDenySip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "10.0.0.0/8" "11.0.0.0/8" ];
-	description = ''
+        description = ''
           Acess control list for denying incoming
-	   SIP registrations and traffic.
+          SIP registrations and traffic.
         '';
       };
 
@@ -87,7 +87,7 @@ in
         type = types.int;
         default = 5060;
         description = ''
-	  Port to listen for incoming SIP messages.
+          Port to listen for incoming SIP messages.
         '';
       };
 
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
index 3560d08520b76..a821b9b6bf652 100644
--- a/nixos/modules/services/misc/snapper.nix
+++ b/nixos/modules/services/misc/snapper.nix
@@ -48,6 +48,8 @@ in
           subvolume = "/home";
           extraConfig = ''
             ALLOW_USERS="alice"
+            TIMELINE_CREATE=yes
+            TIMELINE_CLEANUP=yes
           '';
         };
       };
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
index 3335ed09d40e0..5fa262ca3b945 100644
--- a/nixos/modules/services/misc/svnserve.nix
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -24,8 +24,9 @@ in
       };
 
       svnBaseDir = mkOption {
+        type = types.str;
         default = "/repos";
-	description = "Base directory from which Subversion repositories are accessed.";
+        description = "Base directory from which Subversion repositories are accessed.";
       };
     };
 
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
index 5b7cf3ac46c3c..7990a9f6f4cec 100644
--- a/nixos/modules/services/misc/synergy.nix
+++ b/nixos/modules/services/misc/synergy.nix
@@ -23,12 +23,14 @@ in
 
         screenName = mkOption {
           default = "";
+          type = types.str;
           description = ''
             Use the given name instead of the hostname to identify
             ourselves to the server.
           '';
         };
         serverAddress = mkOption {
+          type = types.str;
           description = ''
             The server address is of the form: [hostname][:port].  The
             hostname must be the address or hostname of the server.  The
@@ -46,10 +48,12 @@ in
         enable = mkEnableOption "the Synergy server (send keyboard and mouse events)";
 
         configFile = mkOption {
+          type = types.path;
           default = "/etc/synergy-server.conf";
           description = "The Synergy server configuration file.";
         };
         screenName = mkOption {
+          type = types.str;
           default = "";
           description = ''
             Use the given name instead of the hostname to identify
@@ -57,6 +61,7 @@ in
           '';
         };
         address = mkOption {
+          type = types.str;
           default = "";
           description = "Address on which to listen for clients.";
         };
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index c6ff540ea12f4..b71250f62e0f3 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -20,6 +20,7 @@ in
       type = types.str;
     };
     binary = mkOption {
+      type = types.path;
       description = "Binary to execute (by default \${weechat}/bin/weechat).";
       example = literalExample ''
         ''${pkgs.weechat}/bin/weechat-headless
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
index 0957920f1a097..cd987eb76c76c 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -70,6 +70,7 @@ in
       description = "Zigbee2mqtt Service";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
+      environment.ZIGBEE2MQTT_DATA = cfg.dataDir;
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/zigbee2mqtt";
         User = "zigbee2mqtt";
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index f6af7c75ebae9..1d12e81a9eca8 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -76,6 +76,7 @@ in {
       default = ''
         zookeeper.root.logger=INFO, CONSOLE
         log4j.rootLogger=INFO, CONSOLE
+        log4j.logger.org.apache.zookeeper.audit.Log4jAuditLogger=INFO, CONSOLE
         log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
         log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
         log4j.appender.CONSOLE.layout.ConversionPattern=[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
@@ -128,11 +129,10 @@ in {
       description = "Zookeeper Daemon";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      environment = { ZOOCFGDIR = configDir; };
       serviceConfig = {
         ExecStart = ''
           ${pkgs.jre}/bin/java \
-            -cp "${cfg.package}/lib/*:${cfg.package}/${cfg.package.name}.jar:${configDir}" \
+            -cp "${cfg.package}/lib/*:${configDir}" \
             ${escapeShellArgs cfg.extraCmdLineOptions} \
             -Dzookeeper.datadir.autocreate=false \
             ${optionalString cfg.preferIPv4 "-Djava.net.preferIPv4Stack=true"} \
@@ -143,6 +143,7 @@ in {
       };
       preStart = ''
         echo "${toString cfg.id}" > ${cfg.dataDir}/myid
+        mkdir -p ${cfg.dataDir}/version-2
       '';
     };
 
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
index 75218aa1d46b0..1dccbc93edf84 100644
--- a/nixos/modules/services/monitoring/apcupsd.nix
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -104,7 +104,7 @@ in
       hooks = mkOption {
         default = {};
         example = {
-          doshutdown = ''# shell commands to notify that the computer is shutting down'';
+          doshutdown = "# shell commands to notify that the computer is shutting down";
         };
         type = types.attrsOf types.lines;
         description = ''
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
new file mode 100644
index 0000000000000..b8b95d846c6af
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -0,0 +1,150 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grafana-image-renderer;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "grafana-image-renderer-config.json" cfg.settings;
+in {
+  options.services.grafana-image-renderer = {
+    enable = mkEnableOption "grafana-image-renderer";
+
+    chromium = mkOption {
+      type = types.package;
+      description = ''
+        The chromium to use for image rendering.
+      '';
+    };
+
+    verbose = mkEnableOption "verbosity for the service";
+
+    provisionGrafana = mkEnableOption "Grafana configuration for grafana-image-renderer";
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = format.type;
+
+        options = {
+          service = {
+            port = mkOption {
+              type = types.port;
+              default = 8081;
+              description = ''
+                The TCP port to use for the rendering server.
+              '';
+            };
+            logging.level = mkOption {
+              type = types.enum [ "error" "warning" "info" "debug" ];
+              default = "info";
+              description = ''
+                The log-level of the <filename>grafana-image-renderer.service</filename>-unit.
+              '';
+            };
+          };
+          rendering = {
+            width = mkOption {
+              default = 1000;
+              type = types.ints.positive;
+              description = ''
+                Width of the PNG used to display the alerting graph.
+              '';
+            };
+            height = mkOption {
+              default = 500;
+              type = types.ints.positive;
+              description = ''
+                Height of the PNG used to display the alerting graph.
+              '';
+            };
+            mode = mkOption {
+              default = "default";
+              type = types.enum [ "default" "reusable" "clustered" ];
+              description = ''
+                Rendering mode of <package>grafana-image-renderer</package>:
+                <itemizedlist>
+                <listitem><para><literal>default:</literal> Creates on browser-instance
+                  per rendering request.</para></listitem>
+                <listitem><para><literal>reusable:</literal> One browser instance
+                  will be started and reused for each rendering request.</para></listitem>
+                <listitem><para><literal>clustered:</literal> allows to precisely
+                  configure how many browser-instances are supposed to be used. The values
+                  for that mode can be declared in <literal>rendering.clustering</literal>.
+                  </para></listitem>
+                </itemizedlist>
+              '';
+            };
+            args = mkOption {
+              type = types.listOf types.str;
+              default = [ "--no-sandbox" ];
+              description = ''
+                List of CLI flags passed to <package>chromium</package>.
+              '';
+            };
+          };
+        };
+      };
+
+      default = {};
+
+      description = ''
+        Configuration attributes for <package>grafana-image-renderer</package>.
+
+        See <link xlink:href="https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json" />
+        for supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.provisionGrafana -> config.services.grafana.enable;
+        message = ''
+          To provision a Grafana instance to use grafana-image-renderer,
+          `services.grafana.enable` must be set to `true`!
+        '';
+      }
+    ];
+
+    services.grafana.extraOptions = mkIf cfg.provisionGrafana {
+      RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
+      RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
+    };
+
+    services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
+
+    services.grafana-image-renderer.settings = {
+      rendering = mapAttrs (const mkDefault) {
+        chromeBin = "${cfg.chromium}/bin/chromium";
+        verboseLogging = cfg.verbose;
+        timezone = config.time.timeZone;
+      };
+
+      service = {
+        logging.level = mkIf cfg.verbose (mkDefault "debug");
+        metrics.enabled = mkDefault false;
+      };
+    };
+
+    systemd.services.grafana-image-renderer = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = " A Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)";
+
+      environment = {
+        PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true";
+      };
+
+      serviceConfig = {
+        DynamicUser = true;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}";
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index b0c81a46d4d81..c8515c4b8988c 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -5,10 +5,11 @@ with lib;
 let
   cfg = config.services.grafana;
   opt = options.services.grafana;
+  declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
 
   envOptions = {
     PATHS_DATA = cfg.dataDir;
-    PATHS_PLUGINS = "${cfg.dataDir}/plugins";
+    PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
     PATHS_LOGS = "${cfg.dataDir}/log";
 
     SERVER_PROTOCOL = cfg.protocol;
@@ -260,6 +261,12 @@ in {
       defaultText = "pkgs.grafana";
       type = types.package;
     };
+    declarativePlugins = mkOption {
+      type = with types; nullOr (listOf path);
+      default = null;
+      description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed.";
+      example = literalExample "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]";
+    };
 
     dataDir = mkOption {
       description = "Data directory.";
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index 64d9d61950da1..9213748d3c9a1 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -25,10 +25,10 @@ let
 
   graphiteApiConfig = pkgs.writeText "graphite-api.yaml" ''
     search_index: ${dataDir}/index
-    ${optionalString (config.time.timeZone != null) ''time_zone: ${config.time.timeZone}''}
-    ${optionalString (cfg.api.finders != []) ''finders:''}
+    ${optionalString (config.time.timeZone != null) "time_zone: ${config.time.timeZone}"}
+    ${optionalString (cfg.api.finders != []) "finders:"}
     ${concatMapStringsSep "\n" (f: "  - " + f.moduleName) cfg.api.finders}
-    ${optionalString (cfg.api.functions != []) ''functions:''}
+    ${optionalString (cfg.api.functions != []) "functions:"}
     ${concatMapStringsSep "\n" (f: "  - " + f) cfg.api.functions}
     ${cfg.api.extraConfig}
   '';
diff --git a/nixos/modules/services/monitoring/incron.nix b/nixos/modules/services/monitoring/incron.nix
index 1789fd9f20514..dc97af58562e7 100644
--- a/nixos/modules/services/monitoring/incron.nix
+++ b/nixos/modules/services/monitoring/incron.nix
@@ -67,7 +67,7 @@ in
   config = mkIf cfg.enable {
 
     warnings = optional (cfg.allow != null && cfg.deny != null)
-      ''If `services.incron.allow` is set then `services.incron.deny` will be ignored.'';
+      "If `services.incron.allow` is set then `services.incron.deny` will be ignored.";
 
     environment.systemPackages = [ pkgs.incron ];
 
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
index f4eec7e0d2841..51cabaa274a3b 100644
--- a/nixos/modules/services/monitoring/loki.nix
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -39,7 +39,7 @@ in {
     };
 
     configuration = mkOption {
-      type = types.attrs;
+      type = (pkgs.formats.json {}).type;
       default = {};
       description = ''
         Specify the configuration for Loki in Nix.
@@ -78,6 +78,8 @@ in {
       '';
     }];
 
+    environment.systemPackages = [ pkgs.grafana-loki ]; # logcli
+
     users.groups.${cfg.group} = { };
     users.users.${cfg.user} = {
       description = "Loki Service User";
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
new file mode 100644
index 0000000000000..7046de9d403cf
--- /dev/null
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mackerel-agent;
+  settingsFmt = pkgs.formats.toml {};
+in {
+  options.services.mackerel-agent = {
+    enable = mkEnableOption "mackerel.io agent";
+
+    # the upstream package runs as root, but doesn't seem to be strictly
+    # necessary for basic functionality
+    runAsRoot = mkEnableOption "Whether to run as root.";
+
+    autoRetirement = mkEnableOption ''
+      Whether to automatically retire the host upon OS shutdown.
+    '';
+
+    apiKeyFile = mkOption {
+      type = types.path;
+      default = "";
+      example = "/run/keys/mackerel-api-key";
+      description = ''
+        Path to file containing the Mackerel API key. The file should contain a
+        single line of the following form:
+
+        <literallayout>apikey = "EXAMPLE_API_KEY"</literallayout>
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        Options for mackerel-agent.conf.
+
+        Documentation:
+        <link xlink:href="https://mackerel.io/docs/entry/spec/agent"/>
+      '';
+
+      default = {};
+      example = {
+        verbose = false;
+        silent = false;
+      };
+
+      type = types.submodule {
+        freeformType = settingsFmt.type;
+
+        options.host_status = {
+          on_start = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent startup.";
+            default = "working";
+          };
+          on_stop = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent shutdown.";
+            default = "poweroff";
+          };
+        };
+
+        options.diagnostic =
+          mkEnableOption "Collect memory usage for the agent itself";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ mackerel-agent ];
+
+    environment.etc = {
+      "mackerel-agent/mackerel-agent.conf".source =
+        settingsFmt.generate "mackerel-agent.conf" cfg.settings;
+      "mackerel-agent/conf.d/api-key.conf".source = cfg.apiKeyFile;
+    };
+
+    services.mackerel-agent.settings = {
+      root = mkDefault "/var/lib/mackerel-agent";
+      pidfile = mkDefault "/run/mackerel-agent/mackerel-agent.pid";
+
+      # conf.d stores the symlink to cfg.apiKeyFile
+      include = mkDefault "/etc/mackerel-agent/conf.d/*.conf";
+    };
+
+    # upstream service file in https://git.io/JUt4Q
+    systemd.services.mackerel-agent = {
+      description = "mackerel.io agent";
+      after = [ "network-online.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        MACKEREL_PLUGIN_WORKDIR = mkDefault "%C/mackerel-agent";
+      };
+      serviceConfig = {
+        DynamicUser = !cfg.runAsRoot;
+        PrivateTmp = mkDefault true;
+        CacheDirectory = "mackerel-agent";
+        ConfigurationDirectory = "mackerel-agent";
+        RuntimeDirectory = "mackerel-agent";
+        StateDirectory = "mackerel-agent";
+        ExecStart = "${pkgs.mackerel-agent}/bin/mackerel-agent supervise";
+        ExecStopPost = mkIf cfg.autoRetirement "${pkg.mackerel-agent}/bin/mackerel-agent retire -force";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitNOFILE = mkDefault 65536;
+        LimitNPROC = mkDefault 65536;
+      };
+      restartTriggers = [
+        config.environment.etc."mackerel-agent/mackerel-agent.conf".source
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 2e73e15d3a867..d5b679097b309 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -77,6 +77,7 @@ in {
           '';
         };
         extraPackages = mkOption {
+          type = types.functionTo (types.listOf types.package);
           default = ps: [];
           defaultText = "ps: []";
           example = literalExample ''
@@ -142,7 +143,7 @@ in {
       serviceConfig = {
         Environment="PYTHONPATH=${cfg.package}/libexec/netdata/python.d/python_modules";
         ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c ${configFile}";
-        ExecReload = "${pkgs.utillinux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
+        ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
         TimeoutStopSec = 60;
         Restart = "on-failure";
         # User and group
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index 98aaa9c0f030a..9103a6f932dbb 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -32,6 +32,8 @@ let
       (pkgs.writeText "prometheus.rules" (concatStringsSep "\n" cfg.rules))
     ]);
     scrape_configs = filterValidPrometheus cfg.scrapeConfigs;
+    remote_write = filterValidPrometheus cfg.remoteWrite;
+    remote_read = filterValidPrometheus cfg.remoteRead;
     alerting = {
       inherit (cfg) alertmanagers;
     };
@@ -45,7 +47,7 @@ let
 
   cmdlineArgs = cfg.extraFlags ++ [
     "--storage.tsdb.path=${workingDir}/data/"
-    "--config.file=${prometheusYml}"
+    "--config.file=/run/prometheus/prometheus-substituted.yaml"
     "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}"
     "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}"
     "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s"
@@ -101,6 +103,157 @@ let
     };
   };
 
+  promTypes.remote_read = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      name = mkOpt types.string ''
+        Name of the remote read config, which if specified must be unique among remote read configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote read configs.
+      '';
+      required_matchers = mkOpt (types.attrsOf types.str) ''
+        An optional list of equality matchers which have to be
+        present in a selector to query the remote read endpoint.
+      '';
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote read endpoint.
+      '';
+      read_recent = mkOpt types.bool ''
+        Whether reads should be made for queries for time ranges that
+        the local storage should have complete data for.
+      '';
+      basic_auth = mkOpt (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description = ''
+              HTTP username
+            '';
+          };
+          password = mkOpt types.str "HTTP password";
+          password_file = mkOpt types.str "HTTP password file";
+        };
+      }) ''
+        Sets the `Authorization` header on every remote read request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote read request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+    };
+  };
+
+  promTypes.remote_write = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote write endpoint.
+      '';
+      write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of remote write relabel configurations.
+      '';
+      name = mkOpt types.string ''
+        Name of the remote write config, which if specified must be unique among remote write configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote write configs.
+      '';
+      basic_auth = mkOpt (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description = ''
+              HTTP username
+            '';
+          };
+          password = mkOpt types.str "HTTP password";
+          password_file = mkOpt types.str "HTTP password file";
+        };
+      }) ''
+        Sets the `Authorization` header on every remote write request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote write request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+      queue_config = mkOpt (types.submodule {
+        options = {
+          capacity = mkOpt types.int ''
+            Number of samples to buffer per shard before we block reading of more
+            samples from the WAL. It is recommended to have enough capacity in each
+            shard to buffer several requests to keep throughput up while processing
+            occasional slow remote requests.
+          '';
+          max_shards = mkOpt types.int ''
+            Maximum number of shards, i.e. amount of concurrency.
+          '';
+          min_shards = mkOpt types.int ''
+            Minimum number of shards, i.e. amount of concurrency.
+          '';
+          max_samples_per_send = mkOpt types.int ''
+            Maximum number of samples per send.
+          '';
+          batch_send_deadline = mkOpt types.str ''
+            Maximum time a sample will wait in buffer.
+          '';
+          min_backoff = mkOpt types.str ''
+            Initial retry delay. Gets doubled for every retry.
+          '';
+          max_backoff = mkOpt types.str ''
+            Maximum retry delay.
+          '';
+        };
+      }) ''
+        Configures the queue used to write to remote storage.
+      '';
+      metadata_config = mkOpt (types.submodule {
+        options = {
+          send = mkOpt types.bool ''
+            Whether metric metadata is sent to remote storage or not.
+          '';
+          send_interval = mkOpt types.str ''
+            How frequently metric metadata is sent to remote storage.
+          '';
+        };
+      }) ''
+        Configures the sending of series metadata to remote storage.
+        Metadata configuration is subject to change at any point
+        or be removed in future releases.
+      '';
+    };
+  };
+
   promTypes.scrape_config = types.submodule {
     options = {
       job_name = mkOption {
@@ -217,6 +370,14 @@ let
         List of file service discovery configurations.
       '';
 
+      gce_sd_configs = mkOpt (types.listOf promTypes.gce_sd_config) ''
+        List of Google Compute Engine service discovery configurations.
+
+        See <link
+        xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config">the
+        relevant Prometheus configuration docs</link> for more detail.
+      '';
+
       static_configs = mkOpt (types.listOf promTypes.static_config) ''
         List of labeled target groups for this job.
       '';
@@ -402,6 +563,52 @@ let
     };
   };
 
+  promTypes.gce_sd_config = types.submodule {
+    options = {
+      # Use `mkOption` instead of `mkOpt` for project and zone because they are
+      # required configuration values for `gce_sd_config`.
+      project = mkOption {
+        type = types.str;
+        description = ''
+          The GCP Project.
+        '';
+      };
+
+      zone = mkOption {
+        type = types.str;
+        description = ''
+          The zone of the scrape targets. If you need multiple zones use multiple
+          gce_sd_configs.
+        '';
+      };
+
+      filter = mkOpt types.str ''
+        Filter can be used optionally to filter the instance list by other
+        criteria Syntax of this filter string is described here in the filter
+        query parameter section: <link
+        xlink:href="https://cloud.google.com/compute/docs/reference/latest/instances/list"
+        />.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the cloud instance list.
+      '';
+
+      port = mkDefOpt types.port "80" ''
+        The port to scrape metrics from. If using the public IP address, this
+        must instead be specified in the relabeling rule.
+      '';
+
+      tag_separator = mkDefOpt types.str "," ''
+        The tag separator used to separate concatenated GCE instance network tags.
+
+        See the GCP documentation on network tags for more information: <link
+        xlink:href="https://cloud.google.com/vpc/docs/add-remove-network-tags"
+        />
+      '';
+    };
+  };
+
   promTypes.relabel_config = types.submodule {
     options = {
       source_labels = mkOpt (types.listOf types.str) ''
@@ -432,10 +639,10 @@ let
         regular expression matches.
       '';
 
-      action = mkDefOpt (types.enum ["replace" "keep" "drop"]) "replace" ''
+      action =
+        mkDefOpt (types.enum ["replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep"]) "replace" ''
         Action to perform based on regex matching.
       '';
-
     };
   };
 
@@ -522,6 +729,45 @@ in {
       '';
     };
 
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/prometheus.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        Environment variables from this file will be interpolated into the
+        config file using envsubst with this syntax:
+        <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+
+        <programlisting>
+          # Example scrape config entry handling an OAuth bearer token
+          {
+            job_name = "home_assistant";
+            metrics_path = "/api/prometheus";
+            scheme = "https";
+            bearer_token = "\''${HOME_ASSISTANT_BEARER_TOKEN}";
+            [...]
+          }
+        </programlisting>
+
+        <programlisting>
+          # Content of the environment file
+          HOME_ASSISTANT_BEARER_TOKEN=someoauthbearertoken
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>Prometheus</literal> is running.
+      '';
+    };
+
     configText = mkOption {
       type = types.nullOr types.lines;
       default = null;
@@ -541,6 +787,24 @@ in {
       '';
     };
 
+    remoteRead = mkOption {
+      type = types.listOf promTypes.remote_read;
+      default = [];
+      description = ''
+        Parameters of the endpoints to query from.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_read">the official documentation</link> for more information.
+      '';
+    };
+
+    remoteWrite = mkOption {
+      type = types.listOf promTypes.remote_write;
+      default = [];
+      description = ''
+        Parameters of the endpoints to send samples to.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write">the official documentation</link> for more information.
+      '';
+    };
+
     rules = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -662,12 +926,19 @@ in {
     systemd.services.prometheus = {
       wantedBy = [ "multi-user.target" ];
       after    = [ "network.target" ];
+      preStart = ''
+         ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/run/prometheus/prometheus-substituted.yaml" \
+                                                  -i "${prometheusYml}"
+      '';
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/prometheus" +
           optionalString (length cmdlineArgs != 0) (" \\\n  " +
             concatStringsSep " \\\n  " cmdlineArgs);
         User = "prometheus";
         Restart  = "always";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        RuntimeDirectory = "prometheus";
+        RuntimeDirectoryMode = "0700";
         WorkingDirectory = workingDir;
         StateDirectory = cfg.stateDir;
       };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 1233e5cdd1a9c..940f281893710 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -23,6 +23,7 @@ let
   exporterOpts = genAttrs [
     "apcupsd"
     "bind"
+    "bird"
     "blackbox"
     "collectd"
     "dnsmasq"
@@ -37,19 +38,25 @@ let
     "modemmanager"
     "nextcloud"
     "nginx"
+    "nginxlog"
     "node"
     "openvpn"
     "postfix"
     "postgres"
+    "py-air-control"
     "redis"
     "rspamd"
+    "rtl_433"
     "snmp"
+    "smokeping"
+    "sql"
     "surfboard"
     "tor"
     "unifi"
     "unifi-poller"
     "varnish"
     "wireguard"
+    "flow"
   ] (name:
     import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; }
   );
@@ -217,16 +224,21 @@ in
         Please specify either 'services.prometheus.exporters.mail.configuration'
           or 'services.prometheus.exporters.mail.configFile'.
       '';
+    } {
+      assertion = cfg.sql.enable -> (
+        (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
+      );
+      message = ''
+        Please specify either 'services.prometheus.exporters.sql.configuration' or
+          'services.prometheus.exporters.sql.configFile'
+      '';
     } ];
   }] ++ [(mkIf config.services.minio.enable {
     services.prometheus.exporters.minio.minioAddress  = mkDefault "http://localhost:9000";
     services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey;
     services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey;
-  })] ++ [(mkIf config.services.rspamd.enable {
-    services.prometheus.exporters.rspamd.url = mkDefault "http://localhost:11334/stat";
-  })] ++ [(mkIf config.services.nginx.enable {
-    systemd.services.prometheus-nginx-exporter.after = [ "nginx.service" ];
-    systemd.services.prometheus-nginx-exporter.requires = [ "nginx.service" ];
+  })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable {
+    hardware.rtl-sdr.enable = mkDefault true;
   })] ++ [(mkIf config.services.postfix.enable {
     services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
   })] ++ (mapAttrsToList (name: conf:
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
new file mode 100644
index 0000000000000..d8a526eafcea9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bird;
+in
+{
+  port = 9324;
+  extraOpts = {
+    birdVersion = mkOption {
+      type = types.enum [ 1 2 ];
+      default = 2;
+      description = ''
+        Specifies whether BIRD1 or BIRD2 is in use.
+      '';
+    };
+    birdSocket = mkOption {
+      type = types.path;
+      default = "/var/run/bird.ctl";
+      description = ''
+        Path to BIRD2 (or BIRD1 v4) socket.
+      '';
+    };
+    newMetricFormat = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Enable the new more-generic metric format.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      SupplementaryGroups = singleton (if cfg.birdVersion == 1 then "bird" else "bird2");
+      ExecStart = ''
+        ${pkgs.prometheus-bird-exporter}/bin/bird_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -bird.socket ${cfg.birdSocket} \
+          -bird.v2=${if cfg.birdVersion == 2 then "true" else "false"} \
+          -format.new=${if cfg.newMetricFormat then "true" else "false"} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 972104630275b..a3b2b92bc3479 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -20,7 +20,7 @@ in
       port = mkOption {
         type = types.int;
         default = 25826;
-        description = ''Network address on which to accept collectd binary network packets.'';
+        description = "Network address on which to accept collectd binary network packets.";
       };
 
       listenAddress = mkOption {
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/flow.nix b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
new file mode 100644
index 0000000000000..6a35f46308fe9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.flow;
+in {
+  port = 9590;
+  extraOpts = {
+    brokers = mkOption {
+      type = types.listOf types.str;
+      example = literalExample ''[ "kafka.example.org:19092" ]'';
+      description = "List of Kafka brokers to connect to.";
+    };
+
+    asn = mkOption {
+      type = types.ints.positive;
+      example = 65542;
+      description = "The ASN being monitored.";
+    };
+
+    partitions = mkOption {
+      type = types.listOf types.int;
+      default = [];
+      description = ''
+        The number of the partitions to consume, none means all.
+      '';
+    };
+
+    topic = mkOption {
+      type = types.str;
+      example = "pmacct.acct";
+      description = "The Kafka topic to consume from.";
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = true;
+      ExecStart = ''
+        ${pkgs.prometheus-flow-exporter}/bin/flow-exporter \
+          -asn ${toString cfg.asn} \
+          -topic ${cfg.topic} \
+          -brokers ${concatStringsSep "," cfg.brokers} \
+          ${optionalString (cfg.partitions != []) "-partitions ${concatStringsSep "," cfg.partitions}"} \
+          -addr ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/json.nix b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
index bd0026b55f721..1800da69a2558 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/json.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
@@ -8,28 +8,36 @@ in
 {
   port = 7979;
   extraOpts = {
-    url = mkOption {
-      type = types.str;
-      description = ''
-        URL to scrape JSON from.
-      '';
-    };
     configFile = mkOption {
       type = types.path;
       description = ''
         Path to configuration file.
       '';
     };
-    listenAddress = {}; # not used
   };
   serviceOpts = {
     serviceConfig = {
       ExecStart = ''
-        ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \
-          --port ${toString cfg.port} \
-          ${cfg.url} ${escapeShellArg cfg.configFile} \
+        ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+          --config.file ${escapeShellArg cfg.configFile} \
+          --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
   };
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the endpoint serving JSON
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
index aee6bd5e66cea..ce7125bf5a838 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
@@ -46,11 +46,11 @@ in
       DynamicUser = false;
       ExecStart = ''
         ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \
-          -a ${cfg.listenAddress}:${toString cfg.port} \
-          -u ${cfg.username} \
-          -t ${cfg.timeout} \
-          -l ${cfg.url} \
-          -p ${escapeShellArg "@${cfg.passwordFile}"} \
+          --addr ${cfg.listenAddress}:${toString cfg.port} \
+          --username ${cfg.username} \
+          --timeout ${cfg.timeout} \
+          --server ${cfg.url} \
+          --password ${escapeShellArg "@${cfg.passwordFile}"} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
index 56cddfc55b719..5ee8c346be1dc 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
@@ -42,7 +42,7 @@ in
       '';
     };
   };
-  serviceOpts = {
+  serviceOpts = mkMerge ([{
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \
@@ -54,7 +54,10 @@ in
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
-  };
+  }] ++ [(mkIf config.services.nginx.enable {
+    after = [ "nginx.service" ];
+    requires = [ "nginx.service" ];
+  })]);
   imports = [
     (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
     (mkRemovedOptionModule [ "insecure" ] ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
new file mode 100644
index 0000000000000..8c1f552d58a7c
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginxlog;
+in {
+  port = 9117;
+  extraOpts = {
+    settings = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        All settings of nginxlog expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: https://github.com/martin-helmich/prometheus-nginxlog-exporter
+
+        The `listen` object is already generated by `port`, `listenAddress` and `metricsEndpoint` and
+        will be merged with the value of `settings` before writting it as JSON.
+      '';
+    };
+
+    metricsEndpoint = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = let
+    listenConfig = {
+      listen = {
+        port = cfg.port;
+        address = cfg.listenAddress;
+        metrics_endpoint = cfg.metricsEndpoint;
+      };
+    };
+    completeConfig = pkgs.writeText "nginxlog-exporter.yaml" (builtins.toJSON (lib.recursiveUpdate listenConfig cfg.settings));
+  in {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-nginxlog-exporter}/bin/prometheus-nginxlog-exporter -config-file ${completeConfig}
+      '';
+      Restart="always";
+      ProtectSystem="full";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
new file mode 100644
index 0000000000000..d9ab99221d9d8
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.py-air-control;
+
+  workingDir = "/var/lib/${cfg.stateDir}";
+
+in
+{
+  port = 9896;
+  extraOpts = {
+    deviceHostname = mkOption {
+      type = types.str;
+      example = "192.168.1.123";
+      description = ''
+        The hostname of the air purification device from which to scrape the metrics.
+      '';
+    };
+    protocol = mkOption {
+      type = types.str;
+      default = "http";
+      description = ''
+        The protocol to use when communicating with the air purification device.
+        Available: [http, coap, plain_coap]
+      '';
+    };
+    stateDir = mkOption {
+      type = types.str;
+      default = "prometheus-py-air-control-exporter";
+      description = ''
+        Directory below <literal>/var/lib</literal> to store runtime data.
+        This directory will be created automatically using systemd's StateDirectory mechanism.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      StateDirectory = cfg.stateDir;
+      WorkingDirectory = workingDir;
+      ExecStart = ''
+        ${pkgs.python3Packages.py-air-control-exporter}/bin/py-air-control-exporter \
+          --host ${cfg.deviceHostname} \
+          --protocol ${cfg.protocol} \
+          --listen-port ${toString cfg.port} \
+          --listen-address ${cfg.listenAddress}
+      '';
+      Environment = [ "HOME=${workingDir}" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
index 1f02ae2072499..78fe120e4d932 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
@@ -10,64 +10,55 @@ let
       echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq '.' > $out
     '';
 
-  generateConfig = extraLabels: (map (path: {
-    name = "rspamd_${replaceStrings [ "." " " ] [ "_" "_" ] path}";
-    path = "$.${path}";
-    labels = extraLabels;
-  }) [
-    "actions.'add header'"
-    "actions.'no action'"
-    "actions.'rewrite subject'"
-    "actions.'soft reject'"
-    "actions.greylist"
-    "actions.reject"
-    "bytes_allocated"
-    "chunks_allocated"
-    "chunks_freed"
-    "chunks_oversized"
-    "connections"
-    "control_connections"
-    "ham_count"
-    "learned"
-    "pools_allocated"
-    "pools_freed"
-    "read_only"
-    "scanned"
-    "shared_chunks_allocated"
-    "spam_count"
-    "total_learns"
-  ]) ++ [{
-    name = "rspamd_statfiles";
-    type = "object";
-    path = "$.statfiles[*]";
-    labels = recursiveUpdate {
-      symbol = "$.symbol";
-      type = "$.type";
-    } extraLabels;
-    values = {
-      revision = "$.revision";
-      size = "$.size";
-      total = "$.total";
-      used = "$.used";
-      languages = "$.languages";
-      users = "$.users";
-    };
-  }];
+  generateConfig = extraLabels: {
+    metrics = (map (path: {
+      name = "rspamd_${replaceStrings [ "." " " ] [ "_" "_" ] path}";
+      path = "$.${path}";
+      labels = extraLabels;
+    }) [
+      "actions.'add header'"
+      "actions.'no action'"
+      "actions.'rewrite subject'"
+      "actions.'soft reject'"
+      "actions.greylist"
+      "actions.reject"
+      "bytes_allocated"
+      "chunks_allocated"
+      "chunks_freed"
+      "chunks_oversized"
+      "connections"
+      "control_connections"
+      "ham_count"
+      "learned"
+      "pools_allocated"
+      "pools_freed"
+      "read_only"
+      "scanned"
+      "shared_chunks_allocated"
+      "spam_count"
+      "total_learns"
+    ]) ++ [{
+      name = "rspamd_statfiles";
+      type = "object";
+      path = "$.statfiles[*]";
+      labels = recursiveUpdate {
+        symbol = "$.symbol";
+        type = "$.type";
+      } extraLabels;
+      values = {
+        revision = "$.revision";
+        size = "$.size";
+        total = "$.total";
+        used = "$.used";
+        languages = "$.languages";
+        users = "$.users";
+      };
+    }];
+  };
 in
 {
   port = 7980;
   extraOpts = {
-    listenAddress = {}; # not used
-
-    url = mkOption {
-      type = types.str;
-      description = ''
-        URL to the rspamd metrics endpoint.
-        Defaults to http://localhost:11334/stat when
-        <option>services.rspamd.enable</option> is true.
-      '';
-    };
-
     extraLabels = mkOption {
       type = types.attrsOf types.str;
       default = {
@@ -84,9 +75,25 @@ in
     };
   };
   serviceOpts.serviceConfig.ExecStart = ''
-    ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \
-      --port ${toString cfg.port} \
-      ${cfg.url} ${prettyJSON (generateConfig cfg.extraLabels)} \
+    ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+      --config.file ${prettyJSON (generateConfig cfg.extraLabels)} \
+      --web.listen-address "${cfg.listenAddress}:${toString cfg.port}" \
       ${concatStringsSep " \\\n  " cfg.extraFlags}
   '';
+
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the rspamd metrics endpoint
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.rspamd-exporter.host:7980/probe?target=http://some.rspamd.host:11334/stat
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
new file mode 100644
index 0000000000000..01e420db38978
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, options }:
+
+let
+  cfg = config.services.prometheus.exporters.rtl_433;
+in
+{
+  port = 9550;
+
+  extraOpts = let
+    mkMatcherOptionType = field: description: with lib.types;
+      listOf (submodule {
+        options = {
+          name = lib.mkOption {
+            type = str;
+            description = "Name to match.";
+          };
+          "${field}" = lib.mkOption {
+            type = int;
+            inherit description;
+          };
+          location = lib.mkOption {
+            type = str;
+            description = "Location to match.";
+          };
+        };
+      });
+  in
+  {
+    rtl433Flags = lib.mkOption {
+      type = lib.types.str;
+      default = "-C si";
+      example = "-C si -R 19";
+      description = ''
+        Flags passed verbatim to rtl_433 binary.
+        Having <literal>-C si</literal> (the default) is recommended since only Celsius temperatures are parsed.
+      '';
+    };
+    channels = lib.mkOption {
+      type = mkMatcherOptionType "channel" "Channel to match.";
+      default = [];
+      example = [
+        { name = "Acurite"; channel = 6543; location = "Kitchen"; }
+      ];
+      description = ''
+        List of channel matchers to export.
+      '';
+    };
+    ids = lib.mkOption {
+      type = mkMatcherOptionType "id" "ID to match.";
+      default = [];
+      example = [
+        { name = "Nexus"; id = 1; location = "Bedroom"; }
+      ];
+      description = ''
+        List of ID matchers to export.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      # rtl-sdr udev rules make supported USB devices +rw by plugdev.
+      SupplementaryGroups = "plugdev";
+      ExecStart = let
+        matchers = (map (m:
+          "--channel_matcher '${m.name},${toString m.channel},${m.location}'"
+        ) cfg.channels) ++ (map (m:
+          "--id_matcher '${m.name},${toString m.id},${m.location}'"
+        ) cfg.ids); in ''
+        ${pkgs.prometheus-rtl_433-exporter}/bin/rtl_433_prometheus \
+          -listen ${cfg.listenAddress}:${toString cfg.port} \
+          -subprocess "${pkgs.rtl_433}/bin/rtl_433 -F json ${cfg.rtl433Flags}" \
+          ${lib.concatStringsSep " \\\n  " matchers} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
new file mode 100644
index 0000000000000..0a7bb9c27be28
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.smokeping;
+  goDuration = types.mkOptionType {
+    name = "goDuration";
+    description = "Go duration (https://golang.org/pkg/time/#ParseDuration)";
+    check = x: types.str.check x && builtins.match "(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+" x != null;
+    inherit (types.str) merge;
+  };
+in
+{
+  port = 9374;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    pingInterval = mkOption {
+      type = goDuration;
+      default = "1s";
+      description = ''
+        Interval between pings.
+      '';
+    };
+    buckets = mkOption {
+      type = types.commas;
+      default = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144";
+      description = ''
+        List of buckets to use for the response duration histogram.
+      '';
+    };
+    hosts = mkOption {
+      type = with types; listOf str;
+      description = ''
+        List of endpoints to probe.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      AmbientCapabilities = [ "CAP_NET_RAW" ];
+      ExecStart = ''
+        ${pkgs.prometheus-smokeping-prober}/bin/smokeping_prober \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --buckets ${cfg.buckets} \
+          --ping.interval ${cfg.pingInterval} \
+          --privileged \
+          ${concatStringsSep " \\\n  " cfg.extraFlags} \
+          ${concatStringsSep " " cfg.hosts}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/sql.nix b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
new file mode 100644
index 0000000000000..d9be724ebc036
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, options }:
+with lib;
+let
+  cfg = config.services.prometheus.exporters.sql;
+  cfgOptions = {
+    options = with types; {
+      jobs = mkOption {
+        type = attrsOf (submodule jobOptions);
+        default = { };
+        description = "An attrset of metrics scraping jobs to run.";
+      };
+    };
+  };
+  jobOptions = {
+    options = with types; {
+      interval = mkOption {
+        type = str;
+        description = ''
+          How often to run this job, specified in
+          <link xlink:href="https://golang.org/pkg/time/#ParseDuration">Go duration</link> format.
+        '';
+      };
+      connections = mkOption {
+        type = listOf str;
+        description = "A list of connection strings of the SQL servers to scrape metrics from";
+      };
+      startupSql = mkOption {
+        type = listOf str;
+        default = [];
+        description = "A list of SQL statements to execute once after making a connection.";
+      };
+      queries = mkOption {
+        type = attrsOf (submodule queryOptions);
+        description = "SQL queries to run.";
+      };
+    };
+  };
+  queryOptions = {
+    options = with types; {
+      help = mkOption {
+        type = nullOr str;
+        default = null;
+        description = "A human-readable description of this metric.";
+      };
+      labels = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = "A set of columns that will be used as Prometheus labels.";
+      };
+      query = mkOption {
+        type = str;
+        description = "The SQL query to run.";
+      };
+      values = mkOption {
+        type = listOf str;
+        description = "A set of columns that will be used as values of this metric.";
+      };
+    };
+  };
+
+  configFile =
+    if cfg.configFile != null
+    then cfg.configFile
+    else
+      let
+        nameInline = mapAttrsToList (k: v: v // { name = k; });
+        renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ];
+        configuration = {
+          jobs = map renameStartupSql
+            (nameInline (mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs));
+        };
+      in
+      builtins.toFile "config.yaml" (builtins.toJSON configuration);
+in
+{
+  extraOpts = {
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        Path to configuration file.
+      '';
+    };
+    configuration = mkOption {
+      type = with types; nullOr (submodule cfgOptions);
+      default = null;
+      description = ''
+        Exporter configuration as nix attribute set. Mutually exclusive with 'configFile' option.
+      '';
+    };
+  };
+
+  port = 9237;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-sql-exporter}/bin/sql_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -config.file ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
index c72b4abfcdce3..3ea254371142b 100644
--- a/nixos/modules/services/monitoring/smartd.nix
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -36,7 +36,7 @@ let
 
       $SMARTD_MESSAGE
       EOF
-      } | ${pkgs.utillinux}/bin/wall 2>/dev/null
+      } | ${pkgs.util-linux}/bin/wall 2>/dev/null
     ''}
     ${optionalString nx.enable ''
       export DISPLAY=${nx.display}
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
index 8d781d82d0864..ce9e57a187cdc 100644
--- a/nixos/modules/services/monitoring/teamviewer.nix
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -31,14 +31,14 @@ in
       after = [ "NetworkManager-wait-online.service" "network.target" ];
       preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer";
 
+      startLimitIntervalSec = 60;
+      startLimitBurst = 10;
       serviceConfig = {
         Type = "forking";
         ExecStart = "${pkgs.teamviewer}/bin/teamviewerd -d";
         PIDFile = "/run/teamviewerd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "on-abort";
-        StartLimitInterval = "60";
-        StartLimitBurst = "10";
       };
     };
   };
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
index 5d131557e8be9..bc30ca3b77cfd 100644
--- a/nixos/modules/services/monitoring/telegraf.nix
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -5,14 +5,8 @@ with lib;
 let
   cfg = config.services.telegraf;
 
-  configFile = pkgs.runCommand "config.toml" {
-    buildInputs = [ pkgs.remarshal ];
-    preferLocalBuild = true;
-  } ''
-    remarshal -if json -of toml \
-      < ${pkgs.writeText "config.json" (builtins.toJSON cfg.extraConfig)} \
-      > $out
-  '';
+  settingsFormat = pkgs.formats.toml {};
+  configFile = settingsFormat.generate "config.toml" cfg.extraConfig;
 in {
   ###### interface
   options = {
@@ -26,22 +20,31 @@ in {
         type = types.package;
       };
 
+      environmentFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = "/run/keys/telegraf.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using envsubst with this syntax:
+          <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+          This is useful to avoid putting secrets into the nix store.
+        '';
+      };
+
       extraConfig = mkOption {
         default = {};
         description = "Extra configuration options for telegraf";
-        type = types.attrs;
+        type = settingsFormat.type;
         example = {
-          outputs = {
-            influxdb = {
-              urls = ["http://localhost:8086"];
-              database = "telegraf";
-            };
+          outputs.influxdb = {
+            urls = ["http://localhost:8086"];
+            database = "telegraf";
           };
-          inputs = {
-            statsd = {
-              service_address = ":8125";
-              delete_timings = true;
-            };
+          inputs.statsd = {
+            service_address = ":8125";
+            delete_timings = true;
           };
         };
       };
@@ -51,15 +54,28 @@ in {
 
   ###### implementation
   config = mkIf config.services.telegraf.enable {
-    systemd.services.telegraf = {
+    systemd.services.telegraf = let
+      finalConfigFile = if config.services.telegraf.environmentFiles == []
+                        then configFile
+                        else "/var/run/telegraf/config.toml";
+    in {
       description = "Telegraf Agent";
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
-        ExecStart=''${cfg.package}/bin/telegraf -config "${configFile}"'';
+        EnvironmentFile = config.services.telegraf.environmentFiles;
+        ExecStartPre = lib.optional (config.services.telegraf.environmentFiles != [])
+          (pkgs.writeShellScript "pre-start" ''
+            umask 077
+            ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /var/run/telegraf/config.toml
+          '');
+        ExecStart="${cfg.package}/bin/telegraf -config ${finalConfigFile}";
         ExecReload="${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = "telegraf";
         User = "telegraf";
         Restart = "on-failure";
+        # for ping probes
+        AmbientCapabilities = [ "CAP_NET_RAW" ];
       };
     };
 
diff --git a/nixos/modules/services/monitoring/thanos.nix b/nixos/modules/services/monitoring/thanos.nix
index 52dab28cf72ff..474ea4b250546 100644
--- a/nixos/modules/services/monitoring/thanos.nix
+++ b/nixos/modules/services/monitoring/thanos.nix
@@ -12,7 +12,7 @@ let
   };
 
   optionToArgs = opt: v  : optional (v != null)  ''--${opt}="${toString v}"'';
-  flagToArgs   = opt: v  : optional v            ''--${opt}'';
+  flagToArgs   = opt: v  : optional v            "--${opt}";
   listToArgs   = opt: vs : map               (v: ''--${opt}="${v}"'') vs;
   attrsToArgs  = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs;
 
@@ -67,7 +67,7 @@ let
     preferLocalBuild = true;
     json = builtins.toFile "${name}.json" (builtins.toJSON attrs);
     nativeBuildInputs = [ pkgs.remarshal ];
-  } ''json2yaml -i $json -o $out'';
+  } "json2yaml -i $json -o $out";
 
   thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" +
     (let args = cfg.${cmd}.arguments;
diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix
index a45e806d4ad86..ae5097c54424c 100644
--- a/nixos/modules/services/monitoring/ups.nix
+++ b/nixos/modules/services/monitoring/ups.nix
@@ -205,7 +205,7 @@ in
       after = [ "upsd.service" ];
       wantedBy = [ "multi-user.target" ];
       # TODO: replace 'root' by another username.
-      script = ''${pkgs.nut}/bin/upsdrvctl -u root start'';
+      script = "${pkgs.nut}/bin/upsdrvctl -u root start";
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index d17959a6a3059..632c3fb1059db 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -28,6 +28,9 @@ let
 
     # Don't start services that are not yet initialized
     unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
+    startLimitBurst =
+      if daemonType == "osd" then 30 else if lib.elem daemonType ["mgr" "mds"] then 3 else 5;
+    startLimitIntervalSec = 60 * 30;  # 30 mins
 
     serviceConfig = {
       LimitNOFILE = 1048576;
@@ -39,22 +42,17 @@ let
       ProtectHome = "true";
       ProtectSystem = "full";
       Restart = "on-failure";
-      StartLimitBurst = "5";
-      StartLimitInterval = "30min";
       StateDirectory = stateDirectory;
       User = "ceph";
       Group = if daemonType == "osd" then "disk" else "ceph";
       ExecStart = ''${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
                     -f --cluster ${clusterName} --id ${daemonId}'';
     } // optionalAttrs (daemonType == "osd") {
-      ExecStartPre = ''${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}'';
-      StartLimitBurst = "30";
+      ExecStartPre = "${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}";
       RestartSec = "20s";
       PrivateDevices = "no"; # osd needs disk access
     } // optionalAttrs ( daemonType == "mon") {
       RestartSec = "10";
-    } // optionalAttrs (lib.elem daemonType ["mgr" "mds"]) {
-      StartLimitBurst = "3";
     };
   });
 
@@ -355,7 +353,7 @@ in
     ];
 
     warnings = optional (cfg.global.monInitialMembers == null)
-      ''Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function'';
+      "Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function";
 
     environment.etc."ceph/ceph.conf".text = let
       # Merge the extraConfig set for mgr daemons, as mgr don't have their own section
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index f298f831fa7b2..2082d513161e3 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -44,6 +44,13 @@ in {
 
       enable = mkEnableOption "Interplanetary File System (WARNING: may cause severe network degredation)";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ipfs;
+        defaultText = "pkgs.ipfs";
+        description = "Which IPFS package to use.";
+      };
+
       user = mkOption {
         type = types.str;
         default = "ipfs";
@@ -176,7 +183,7 @@ in {
   ###### implementation
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.ipfs ];
+    environment.systemPackages = [ cfg.package ];
     environment.variables.IPFS_PATH = cfg.dataDir;
 
     programs.fuse = mkIf cfg.autoMount {
@@ -207,14 +214,14 @@ in {
       "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
-    systemd.packages = [ pkgs.ipfs ];
+    systemd.packages = [ cfg.package ];
 
     systemd.services.ipfs-init = {
       description = "IPFS Initializer";
 
       environment.IPFS_PATH = cfg.dataDir;
 
-      path = [ pkgs.ipfs ];
+      path = [ cfg.package ];
 
       script = ''
         if [[ ! -f ${cfg.dataDir}/config ]]; then
@@ -239,7 +246,7 @@ in {
     };
 
     systemd.services.ipfs = {
-      path = [ "/run/wrappers" pkgs.ipfs ];
+      path = [ "/run/wrappers" cfg.package ];
       environment.IPFS_PATH = cfg.dataDir;
 
       wants = [ "ipfs-init.service" ];
@@ -267,7 +274,7 @@ in {
               cfg.extraConfig))
           );
       serviceConfig = {
-        ExecStart = ["" "${pkgs.ipfs}/bin/ipfs daemon ${ipfsFlags}"];
+        ExecStart = ["" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}"];
         User = cfg.user;
         Group = cfg.group;
       } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
diff --git a/nixos/modules/services/network-filesystems/netatalk.nix b/nixos/modules/services/network-filesystems/netatalk.nix
index 7674c8f7fa8d1..33e851210bc6f 100644
--- a/nixos/modules/services/network-filesystems/netatalk.nix
+++ b/nixos/modules/services/network-filesystems/netatalk.nix
@@ -46,6 +46,7 @@ in
       enable = mkEnableOption "the Netatalk AFP fileserver";
 
       port = mkOption {
+        type = types.port;
         default = 548;
         description = "TCP port to be used for AFP.";
       };
@@ -68,6 +69,7 @@ in
         };
 
         path = mkOption {
+          type = types.str;
           default = "";
           example = "afp-data";
           description = "Share not the whole user home but this subdirectory path.";
@@ -75,6 +77,7 @@ in
 
         basedirRegex = mkOption {
           example = "/home";
+          type = types.str;
           description = "Regex which matches the parent directory of the user homes.";
         };
 
@@ -108,10 +111,10 @@ in
 
       extmap = mkOption {
         type = types.lines;
-	default = "";
-	description = ''
-	  File name extension mappings.
-	  See <literal>man extmap.conf</literal> for more information.
+        default = "";
+        description = ''
+          File name extension mappings.
+          See <literal>man extmap.conf</literal> for more information.
         '';
       };
 
@@ -132,10 +135,10 @@ in
         Type = "forking";
         GuessMainPID = "no";
         PIDFile = "/run/lock/netatalk";
-	ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 0755 -p /var/lib/netatalk/CNID";
+        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 0755 -p /var/lib/netatalk/CNID";
         ExecStart  = "${pkgs.netatalk}/sbin/netatalk -F ${afpConfFile}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP  $MAINPID";
-	ExecStop   = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        ExecStop   = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
         Restart = "always";
         RestartSec = 1;
       };
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
index 677111814a018..03884cb729760 100644
--- a/nixos/modules/services/network-filesystems/openafs/client.nix
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -244,7 +244,7 @@ in
       # postStop, then we get a hang + kernel oops, because AFS can't be
       # stopped simply by sending signals to processes.
       preStop = ''
-        ${pkgs.utillinux}/bin/umount ${cfg.mountPoint}
+        ${pkgs.util-linux}/bin/umount ${cfg.mountPoint}
         ${openafsBin}/sbin/afsd -shutdown
         ${pkgs.kmod}/sbin/rmmod libafs
       '';
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index 095024d2c8af0..4fce650b01336 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -61,6 +61,7 @@ in {
       };
 
       advertisedAddresses = mkOption {
+        type = types.listOf types.str;
         default = [];
         description = "List of IP addresses this server is advertised under. See NetInfo(5)";
       };
@@ -251,7 +252,6 @@ in {
         wantedBy = [ "multi-user.target" ];
         restartIfChanged = false;
         unitConfig.ConditionPathExists = [
-          "|/etc/openafs/server/rxkad.keytab"
           "|/etc/openafs/server/KeyFileExt"
         ];
         preStart = ''
diff --git a/nixos/modules/services/network-filesystems/rsyncd.nix b/nixos/modules/services/network-filesystems/rsyncd.nix
index 9f1263ddff56f..edac86eb0e30d 100644
--- a/nixos/modules/services/network-filesystems/rsyncd.nix
+++ b/nixos/modules/services/network-filesystems/rsyncd.nix
@@ -46,6 +46,13 @@ in {
         '';
       };
 
+      socketActivated = mkOption {
+        default = false;
+        type = types.bool;
+        description =
+          "If enabled Rsync will be socket-activated rather than run persistently.";
+      };
+
     };
   };
 
@@ -63,12 +70,55 @@ in {
 
     services.rsyncd.settings.global.port = toString cfg.port;
 
-    systemd.services.rsyncd = {
-      description = "Rsync daemon";
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart =
-        "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}";
+    systemd = let
+      serviceConfigSecurity = {
+        ProtectSystem = "full";
+        PrivateDevices = "on";
+        NoNewPrivileges = "on";
+      };
+    in {
+      services.rsync = {
+        enable = !cfg.socketActivated;
+        aliases = [ "rsyncd" ];
+
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
+        documentation = [ "man:rsync(1)" "man:rsyncd.conf(5)" ];
+
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart =
+            "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}";
+          RestartSec = 1;
+        };
+
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      services."rsync@" = {
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
+
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart = "${pkgs.rsync}/bin/rsync --daemon --config=${configFile}";
+          StandardInput = "socket";
+          StandardOutput = "inherit";
+          StandardError = "journal";
+        };
+      };
+
+      sockets.rsync = {
+        enable = cfg.socketActivated;
+
+        description = "socket for fast remote file copy program daemon";
+        conflicts = [ "rsync.service" ];
+
+        listenStreams = [ (toString cfg.port) ];
+        socketConfig.Accept = true;
+
+        wantedBy = [ "sockets.target" ];
+      };
     };
+
   };
 
   meta.maintainers = with lib.maintainers; [ ehmry ];
diff --git a/nixos/modules/services/network-filesystems/samba-wsdd.nix b/nixos/modules/services/network-filesystems/samba-wsdd.nix
new file mode 100644
index 0000000000000..c68039c79e2b7
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/samba-wsdd.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.samba-wsdd;
+
+in {
+  options = {
+    services.samba-wsdd = {
+      enable = mkEnableOption ''
+        Enable Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
+        to be found by Web Service Discovery Clients like Windows.
+        <note>
+          <para>If you use the firewall consider adding the following:</para>
+          <programlisting>
+            networking.firewall.allowedTCPPorts = [ 5357 ];
+            networking.firewall.allowedUDPPorts = [ 3702 ];
+          </programlisting>
+        </note>
+      '';
+      interface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "eth0";
+        description = "Interface or address to use.";
+      };
+      hoplimit = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 2;
+        description = "Hop limit for multicast packets (default = 1).";
+      };
+      workgroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "HOME";
+        description = "Set workgroup name (default WORKGROUP).";
+      };
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "FILESERVER";
+        description = "Override (NetBIOS) hostname to be used (default hostname).";
+      };
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Set domain name (disables workgroup).";
+      };
+      discovery = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable discovery operation mode.";
+      };
+      listen = mkOption {
+        type = types.str;
+        default = "/run/wsdd/wsdd.sock";
+        description = "Listen on path or localhost port in discovery mode.";
+      };
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [ "--shortlog" ];
+        example = [ "--verbose" "--no-http" "--ipv4only" "--no-host" ];
+        description = "Additional wsdd options.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.wsdd ];
+
+    systemd.services.samba-wsdd = {
+      description = "Web Services Dynamic Discovery host daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Type = "simple";
+        ExecStart = ''
+          ${pkgs.wsdd}/bin/wsdd ${optionalString (cfg.interface != null) "--interface '${cfg.interface}'"} \
+                                ${optionalString (cfg.hoplimit != null) "--hoplimit '${toString cfg.hoplimit}'"} \
+                                ${optionalString (cfg.workgroup != null) "--workgroup '${cfg.workgroup}'"} \
+                                ${optionalString (cfg.hostname != null) "--hostname '${cfg.hostname}'"} \
+                                ${optionalString (cfg.domain != null) "--domain '${cfg.domain}'"} \
+                                ${optionalString cfg.discovery "--discovery --listen '${cfg.listen}'"} \
+                                ${escapeShellArgs cfg.extraOptions}
+        '';
+        # Runtime directory and mode
+        RuntimeDirectory = "wsdd";
+        RuntimeDirectoryMode = "0750";
+        # Access write directories
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        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" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @resources @swap";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/xtreemfs.nix b/nixos/modules/services/network-filesystems/xtreemfs.nix
index b8f8c1d71174b..6cc8a05ee00b0 100644
--- a/nixos/modules/services/network-filesystems/xtreemfs.nix
+++ b/nixos/modules/services/network-filesystems/xtreemfs.nix
@@ -92,6 +92,7 @@ in
       enable = mkEnableOption "XtreemFS";
 
       homeDir = mkOption {
+        type = types.path;
         default = "/var/lib/xtreemfs";
         description = ''
           XtreemFS home dir for the xtreemfs user.
@@ -109,19 +110,22 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e40";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32638;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP).
           '';
         };
         address = mkOption {
+          type = types.str;
           example = "127.0.0.1";
           default = "";
           description = ''
@@ -131,12 +135,14 @@ in
         };
         httpPort = mkOption {
           default = 30638;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
           '';
         };
         syncMode = mkOption {
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "ASYNC" ];
           default = "FSYNC";
           example = "FDATASYNC";
           description = ''
@@ -229,20 +235,23 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e41";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32636;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP).
           '';
         };
         address = mkOption {
           example = "127.0.0.1";
+          type = types.str;
           default = "";
           description = ''
             If specified, it defines the interface to listen on. If not
@@ -251,6 +260,7 @@ in
         };
         httpPort = mkOption {
           default = 30636;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
@@ -258,6 +268,7 @@ in
         };
         syncMode = mkOption {
           default = "FSYNC";
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "ASYNC" ];
           example = "FDATASYNC";
           description = ''
             The sync mode influences how operations are committed to the disk
@@ -367,20 +378,23 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e42";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32640;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP and UDP).
           '';
         };
         address = mkOption {
           example = "127.0.0.1";
+          type = types.str;
           default = "";
           description = ''
             If specified, it defines the interface to listen on. If not
@@ -389,6 +403,7 @@ in
         };
         httpPort = mkOption {
           default = 30640;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
diff --git a/nixos/modules/services/network-filesystems/yandex-disk.nix b/nixos/modules/services/network-filesystems/yandex-disk.nix
index cc73f13bf77ac..a5b1f9d4ab630 100644
--- a/nixos/modules/services/network-filesystems/yandex-disk.nix
+++ b/nixos/modules/services/network-filesystems/yandex-disk.nix
@@ -46,12 +46,14 @@ in
 
       user = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           The user the yandex-disk daemon should run as.
         '';
       };
 
       directory = mkOption {
+        type = types.path;
         default = "/home/Yandex.Disk";
         description = "The directory to use for Yandex.Disk storage";
       };
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
index 1128ee2c3e614..39320643dd5e1 100644
--- a/nixos/modules/services/networking/amuled.nix
+++ b/nixos/modules/services/networking/amuled.nix
@@ -24,13 +24,15 @@ in
       };
 
       dataDir = mkOption {
-        default = ''/home/${user}/'';
+        type = types.str;
+        default = "/home/${user}/";
         description = ''
           The directory holding configuration, incoming and temporary files.
         '';
       };
 
       user = mkOption {
+        type = types.nullOr types.str;
         default = null;
         description = ''
           The user the AMule daemon should run as.
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index c876b252e8cd9..0b7d5575c11fc 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -86,7 +86,8 @@ in
 
     ipv6 = mkOption {
       type = types.bool;
-      default = false;
+      default = config.networking.enableIPv6;
+      defaultText = "config.networking.enableIPv6";
       description = "Whether to use IPv6.";
     };
 
@@ -239,7 +240,7 @@ in
 
     system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
     system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
-      [ "mdns_minimal [NOTFOUND=return]" ]
+      (mkOrder 900 [ "mdns_minimal [NOTFOUND=return]" ]) # must be before resolve
       (mkOrder 1501 [ "mdns" ]) # 1501 to ensure it's after dns
     ]);
 
diff --git a/nixos/modules/services/networking/babeld.nix b/nixos/modules/services/networking/babeld.nix
index e62c74d0069df..272c58ecd7ffb 100644
--- a/nixos/modules/services/networking/babeld.nix
+++ b/nixos/modules/services/networking/babeld.nix
@@ -69,6 +69,7 @@ in
 
       extraConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Options that will be copied to babeld.conf.
           See <citerefentry><refentrytitle>babeld</refentrytitle><manvolnum>8</manvolnum></citerefentry> for details.
@@ -87,9 +88,37 @@ in
       description = "Babel routing daemon";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile} -I /run/babeld/babeld.pid -S /var/lib/babeld/state";
+        CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
+        IPAddressAllow = [ "fe80::/64" "ff00::/8" "::1/128" "127.0.0.0/8" ];
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        MemoryDenyWriteExecute = true;
+        ProtectSystem = "strict";
+        ProtectClock = true;
+        ProtectKernelTunables = false; # Couldn't write sysctl: Read-only file system
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false; # kernel_route(ADD): Operation not permitted
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" ];
+        UMask = "0177";
+        RuntimeDirectory = "babeld";
+        StateDirectory = "babeld";
+      };
     };
-
   };
-
 }
diff --git a/nixos/modules/services/networking/bee-clef.nix b/nixos/modules/services/networking/bee-clef.nix
new file mode 100644
index 0000000000000..719714b289827
--- /dev/null
+++ b/nixos/modules/services/networking/bee-clef.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+# NOTE for now nothing is installed into /etc/bee-clef/. the config files are used as read-only from the nix store.
+
+with lib;
+let
+  cfg = config.services.bee-clef;
+in {
+  meta = {
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee-clef = {
+      enable = mkEnableOption "clef external signer instance for Ethereum Swarm Bee";
+
+      dataDir = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef";
+        description = ''
+          Data dir for bee-clef. Beware that some helper scripts may not work when changed!
+          The service itself should work fine, though.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef/password";
+        description = "Password file for bee-clef.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          User the bee-clef daemon should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          Group the bee-clef daemon should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    # if we ever want to have rules.js under /etc/bee-clef/
+    # environment.etc."bee-clef/rules.js".source = ${pkgs.bee-clef}/rules.js
+
+    systemd.packages = [ pkgs.bee-clef ]; # include the upstream bee-clef.service file
+
+    systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}/'         0750 ${cfg.user} ${cfg.group}"
+        "d '${cfg.dataDir}/keystore' 0700 ${cfg.user} ${cfg.group}"
+      ];
+
+    systemd.services.bee-clef = {
+      path = [
+        # these are needed for the ensure-clef-account script
+        pkgs.coreutils
+        pkgs.gnused
+        pkgs.gawk
+      ];
+
+      wantedBy = [ "bee.service" "multi-user.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStartPre = ''${pkgs.bee-clef}/share/bee-clef/ensure-clef-account "${cfg.dataDir}" "${pkgs.bee-clef}/share/bee-clef/"'';
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service start"
+        ];
+        ExecStop = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service stop"
+        ];
+        Environment = [
+          "CONFIGDIR=${cfg.dataDir}"
+          "PASSWORD_FILE=${cfg.passwordFile}"
+        ];
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee-clef") {
+      bee-clef = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        isSystemUser = true;
+        description = "Daemon user for the bee-clef service";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee-clef") {
+      bee-clef = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bee.nix b/nixos/modules/services/networking/bee.nix
new file mode 100644
index 0000000000000..8a77ce23ab4d6
--- /dev/null
+++ b/nixos/modules/services/networking/bee.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.bee;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "bee.yaml" cfg.settings;
+in {
+  meta = {
+    # doc = ./bee.xml;
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee = {
+      enable = mkEnableOption "Ethereum Swarm Bee";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bee;
+        defaultText = "pkgs.bee";
+        example = "pkgs.bee-unstable";
+        description = "The package providing the bee binary for the service.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        description = ''
+          Ethereum Swarm Bee configuration. Refer to
+          <link xlink:href="https://gateway.ethswarm.org/bzz/docs.swarm.eth/docs/installation/configuration/"/>
+          for details on supported values.
+        '';
+      };
+
+      daemonNiceLevel = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Daemon process priority for bee.
+          0 is the default Unix process priority, 19 is the lowest.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          User the bee binary should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          Group the bee binary should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = (hasAttr "password" cfg.settings) != true;
+        message = ''
+          `services.bee.settings.password` is insecure. Use `services.bee.settings.password-file` or `systemd.services.bee.serviceConfig.EnvironmentFile` instead.
+        '';
+      }
+      { assertion = (hasAttr "swap-endpoint" cfg.settings) || (cfg.settings.swap-enable or true == false);
+        message = ''
+          In a swap-enabled network a working Ethereum blockchain node is required. You must specify one using `services.bee.settings.swap-endpoint`, or disable `services.bee.settings.swap-enable` = false.
+        '';
+      }
+    ];
+
+    warnings = optional (! config.services.bee-clef.enable) "The bee service requires an external signer. Consider setting `config.services.bee-clef.enable` = true";
+
+    services.bee.settings = {
+      data-dir             = lib.mkDefault "/var/lib/bee";
+      password-file        = lib.mkDefault "/var/lib/bee/password";
+      clef-signer-enable   = lib.mkDefault true;
+      clef-signer-endpoint = lib.mkDefault "/var/lib/bee-clef/clef.ipc";
+      swap-endpoint        = lib.mkDefault "https://rpc.slock.it/goerli";
+    };
+
+    systemd.packages = [ cfg.package ]; # include the upstream bee.service file
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.settings.data-dir}' 0750 ${cfg.user} ${cfg.group}"
+    ];
+
+    systemd.services.bee = {
+      requires = optional config.services.bee-clef.enable
+        "bee-clef.service";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Nice = cfg.daemonNiceLevel;
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${cfg.package}/bin/bee --config=${configFile} start"
+        ];
+      };
+
+      preStart = with cfg.settings; ''
+        if ! test -f ${password-file}; then
+          < /dev/urandom tr -dc _A-Z-a-z-0-9 2> /dev/null | head -c32 > ${password-file}
+          chmod 0600 ${password-file}
+          echo "Initialized ${password-file} from /dev/urandom"
+        fi
+        if [ ! -f ${data-dir}/keys/libp2p.key ]; then
+          ${cfg.package}/bin/bee init --config=${configFile} >/dev/null
+          echo "
+Logs:   journalctl -f -u bee.service
+
+Bee has SWAP enabled by default and it needs ethereum endpoint to operate.
+It is recommended to use external signer with bee.
+Check documentation for more info:
+- SWAP https://docs.ethswarm.org/docs/installation/manual#swap-bandwidth-incentives
+- External signer https://docs.ethswarm.org/docs/installation/bee-clef
+
+After you finish configuration run 'sudo bee-get-addr'."
+        fi
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee") {
+      bee = {
+        group = cfg.group;
+        home = cfg.settings.data-dir;
+        isSystemUser = true;
+        description = "Daemon user for Ethereum Swarm Bee";
+        extraGroups = optional config.services.bee-clef.enable
+          config.services.bee-clef.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee") {
+      bee = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index faad886357590..e507e8ce9eebc 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -8,6 +8,35 @@ let
 
   bindUser = "named";
 
+  bindZoneOptions = {
+    name = mkOption {
+      type = types.str;
+      description = "Name of the zone.";
+    };
+    master = mkOption {
+      description = "Master=false means slave server";
+      type = types.bool;
+    };
+    file = mkOption {
+      type = types.either types.str types.path;
+      description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
+    };
+    masters = mkOption {
+      type = types.listOf types.str;
+      description = "List of servers for inclusion in stub and secondary zones.";
+    };
+    slaves = mkOption {
+      type = types.listOf types.str;
+      description = "Addresses who may request zone transfers.";
+      default = [];
+    };
+    extraConfig = mkOption {
+      type = types.str;
+      description = "Extra zone config to be appended at the end of the zone section.";
+      default = "";
+    };
+  };
+
   confFile = pkgs.writeText "named.conf"
     ''
       include "/etc/bind/rndc.key";
@@ -72,6 +101,7 @@ in
 
       cacheNetworks = mkOption {
         default = ["127.0.0.0/24"];
+        type = types.listOf types.str;
         description = "
           What networks are allowed to use us as a resolver.  Note
           that this is for recursive queries -- all networks are
@@ -83,6 +113,7 @@ in
 
       blockedNetworks = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = "
           What networks are just blocked.
         ";
@@ -90,6 +121,7 @@ in
 
       ipv4Only = mkOption {
         default = false;
+        type = types.bool;
         description = "
           Only use ipv4, even if the host supports ipv6.
         ";
@@ -97,6 +129,7 @@ in
 
       forwarders = mkOption {
         default = config.networking.nameservers;
+        type = types.listOf types.str;
         description = "
           List of servers we should forward requests to.
         ";
@@ -120,10 +153,9 @@ in
 
       zones = mkOption {
         default = [];
+        type = types.listOf (types.submodule [ { options = bindZoneOptions; } ]);
         description = "
           List of zones we claim authority over.
-            master=false means slave server; slaves means addresses
-           who may request zone transfer.
         ";
         example = [{
           name = "example.com";
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 9ebf382fce42f..59ad9e5468631 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -58,6 +58,7 @@ in
       };
 
       interface = mkOption {
+        type = types.str;
         default = "127.0.0.1";
         description = ''
           The interface the BitlBee deamon will be listening to.  If `127.0.0.1',
@@ -68,6 +69,7 @@ in
 
       portNumber = mkOption {
         default = 6667;
+        type = types.int;
         description = ''
           Number of the port BitlBee will be listening to.
         '';
@@ -142,6 +144,7 @@ in
 
       extraSettings = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Will be inserted in the Settings section of the config file.
         '';
@@ -149,6 +152,7 @@ in
 
       extraDefaults = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Will be inserted in the Default section of the config file.
         '';
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
index dde24522756af..ca323e495ec1d 100644
--- a/nixos/modules/services/networking/blockbook-frontend.nix
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -158,15 +158,21 @@ let
         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;
+          "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";
+          "parse" = true;
+          "subversion" = "";
+          "address_format" = "";
+          "xpub_magic" = 76067358;
+          "xpub_magic_segwit_p2sh" = 77429938;
+          "xpub_magic_segwit_native" = 78792518;
+          "mempool_workers" = 8;
+          "mempool_sub_workers" = 2;
+          "block_addresses_to_keep" = 300;
         }'';
         description = ''
           Additional configurations to be appended to <filename>coin.conf</filename>.
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
index 5f8ac96b2292c..f116d6392ea7d 100644
--- a/nixos/modules/services/networking/cjdns.nix
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -264,10 +264,10 @@ in
          ''
       );
 
+      startLimitIntervalSec = 0;
       serviceConfig = {
         Type = "forking";
         Restart = "always";
-        StartLimitInterval = 0;
         RestartSec = 1;
         CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
         ProtectSystem = true;
diff --git a/nixos/modules/services/networking/cntlm.nix b/nixos/modules/services/networking/cntlm.nix
index 5b5068e43d7c1..eea28e12ce0ea 100644
--- a/nixos/modules/services/networking/cntlm.nix
+++ b/nixos/modules/services/networking/cntlm.nix
@@ -36,19 +36,21 @@ in
     enable = mkEnableOption "cntlm, which starts a local proxy";
 
     username = mkOption {
+      type = types.str;
       description = ''
         Proxy account name, without the possibility to include domain name ('at' sign is interpreted literally).
       '';
     };
 
     domain = mkOption {
-      description = ''Proxy account domain/workgroup name.'';
+      type = types.str;
+      description = "Proxy account domain/workgroup name.";
     };
 
     password = mkOption {
       default = "/etc/cntlm.password";
       type = types.str;
-      description = ''Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.'';
+      description = "Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.";
     };
 
     netbios_hostname = mkOption {
@@ -60,6 +62,7 @@ in
     };
 
     proxy = mkOption {
+      type = types.listOf types.str;
       description = ''
         A list of NTLM/NTLMv2 authenticating HTTP proxies.
 
@@ -75,11 +78,13 @@ in
         A list of domains where the proxy is skipped.
       '';
       default = [];
+      type = types.listOf types.str;
       example = [ "*.example.com" "example.com" ];
     };
 
     port = mkOption {
       default = [3128];
+      type = types.listOf types.port;
       description = "Specifies on which ports the cntlm daemon listens.";
     };
 
diff --git a/nixos/modules/services/networking/connman.nix b/nixos/modules/services/networking/connman.nix
index 6ccc2dffb2676..11f66b05df12c 100644
--- a/nixos/modules/services/networking/connman.nix
+++ b/nixos/modules/services/networking/connman.nix
@@ -42,8 +42,7 @@ in {
 
       extraConfig = mkOption {
         type = types.lines;
-        default = ''
-        '';
+        default = "";
         description = ''
           Configuration lines appended to the generated connman configuration file.
         '';
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index f7d2afead06ca..bfaea4e167c27 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -99,6 +99,7 @@ in
 
       extraConfig = mkOption {
         default = { };
+        type = types.attrsOf types.anything;
         description = ''
           Extra configuration options which are serialized to json and added
           to the config.json file.
diff --git a/nixos/modules/services/networking/corerad.nix b/nixos/modules/services/networking/corerad.nix
index d90a5923bc62e..4acdd1d69cc49 100644
--- a/nixos/modules/services/networking/corerad.nix
+++ b/nixos/modules/services/networking/corerad.nix
@@ -4,13 +4,7 @@ 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";
+  settingsFormat = pkgs.formats.toml {};
 
 in {
   meta.maintainers = with maintainers; [ mdlayher ];
@@ -19,7 +13,7 @@ in {
     enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
 
     settings = mkOption {
-      type = types.uniq types.attrs;
+      type = settingsFormat.type;
       example = literalExample ''
         {
           interfaces = [
@@ -64,7 +58,7 @@ in {
 
   config = mkIf cfg.enable {
     # Prefer the config file over settings if both are set.
-    services.corerad.configFile = mkDefault (writeTOML "corerad.toml" cfg.settings);
+    services.corerad.configFile = mkDefault (settingsFormat.generate "corerad.toml" cfg.settings);
 
     systemd.services.corerad = {
       description = "CoreRAD IPv6 NDP RA daemon";
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 0507b739d4999..31e4b6ad2988c 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -69,6 +69,11 @@ let
         if-carrier-up = "";
       }.${cfg.wait}}
 
+      ${optionalString (config.networking.enableIPv6 == false) ''
+        # Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
+        noipv6
+      ''}
+
       ${cfg.extraConfig}
     '';
 
@@ -186,9 +191,8 @@ in
       { description = "DHCP Client";
 
         wantedBy = [ "multi-user.target" ] ++ optional (!hasDefaultGatewaySet) "network-online.target";
-        wants = [ "network.target" "systemd-udev-settle.service" ];
+        wants = [ "network.target" ];
         before = [ "network-online.target" ];
-        after = [ "systemd-udev-settle.service" ];
 
         restartTriggers = [ exitHook ];
 
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
index dda61212216ce..72965c267a861 100644
--- a/nixos/modules/services/networking/dnscrypt-proxy2.nix
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -27,6 +27,16 @@ in
       default = {};
     };
 
+    upstreamDefaults = mkOption {
+      description = ''
+        Whether to base the config declared in <literal>services.dnscrypt-proxy2.settings</literal> on the upstream example config (<link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>)
+
+        Disable this if you want to declare your dnscrypt config from scratch.
+      '';
+      type = types.bool;
+      default = true;
+    };
+
     configFile = mkOption {
       description = ''
         Path to TOML config file. See: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
@@ -38,7 +48,13 @@ in
         json = builtins.toJSON cfg.settings;
         passAsFile = [ "json" ];
       } ''
-        ${pkgs.remarshal}/bin/json2toml < $jsonPath > $out
+        ${if cfg.upstreamDefaults then ''
+          ${pkgs.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy2.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
+          ${pkgs.jq}/bin/jq --slurp add example.json $jsonPath > config.json # merges the two
+        '' else ''
+          cp $jsonPath config.json
+        ''}
+        ${pkgs.remarshal}/bin/json2toml < config.json > $out
       '';
       defaultText = literalExample "TOML file generated from services.dnscrypt-proxy2.settings";
     };
@@ -71,6 +87,7 @@ in
         NoNewPrivileges = true;
         NonBlocking = true;
         PrivateDevices = true;
+        ProtectClock = true;
         ProtectControlGroups = true;
         ProtectHome = true;
         ProtectHostname = true;
@@ -91,8 +108,12 @@ in
         SystemCallFilter = [
           "@system-service"
           "@chown"
+          "~@aio"
+          "~@keyring"
+          "~@memlock"
           "~@resources"
-          "@privileged"
+          "~@setuid"
+          "~@timer"
         ];
       };
     };
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index b9333cd19a2a5..89360f4bf3732 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -55,7 +55,10 @@ let
   rotateKeys = ''
     # check if keys are not expired
     keyValid() {
-      fingerprint=$(dnscrypt-wrapper --show-provider-publickey | awk '{print $(NF)}')
+      fingerprint=$(dnscrypt-wrapper \
+        --show-provider-publickey \
+        --provider-publickey-file=${publicKey} \
+        | awk '{print $(NF)}')
       dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
         --resolver-address=127.0.0.1:${toString cfg.port} \
         --provider-name=${cfg.providerName} \
@@ -80,7 +83,7 @@ let
   # correctly implement key rotation of dnscrypt-wrapper ephemeral keys.
   dnscrypt-proxy1 = pkgs.callPackage
     ({ stdenv, fetchFromGitHub, autoreconfHook
-    , pkgconfig, libsodium, ldns, openssl, systemd }:
+    , pkg-config, libsodium, ldns, openssl, systemd }:
 
     stdenv.mkDerivation rec {
       pname = "dnscrypt-proxy";
@@ -95,7 +98,7 @@ let
 
       configureFlags = optional stdenv.isLinux "--with-systemd";
 
-      nativeBuildInputs = [ autoreconfHook pkgconfig ];
+      nativeBuildInputs = [ autoreconfHook pkg-config ];
 
       # <ldns/ldns.h> depends on <openssl/ssl.h>
       buildInputs = [ libsodium openssl.dev ldns ] ++ optional stdenv.isLinux systemd;
diff --git a/nixos/modules/services/networking/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
index 8249da69bc1a8..3584915d0aa3a 100644
--- a/nixos/modules/services/networking/dnsdist.nix
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -7,7 +7,7 @@ let
   configFile = pkgs.writeText "dndist.conf" ''
     setLocal('${cfg.listenAddress}:${toString cfg.listenPort}')
     ${cfg.extraConfig}
-    '';
+  '';
 in {
   options = {
     services.dnsdist = {
@@ -26,8 +26,7 @@ in {
 
       extraConfig = mkOption {
         type = types.lines;
-        default = ''
-        '';
+        default = "";
         description = ''
           Extra lines to be added verbatim to dnsdist.conf.
         '';
@@ -35,25 +34,19 @@ in {
     };
   };
 
-  config = mkIf config.services.dnsdist.enable {
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.dnsdist ];
+
     systemd.services.dnsdist = {
-      description = "dnsdist load balancer";
       wantedBy = [ "multi-user.target" ];
-      after = ["network.target"];
 
+      startLimitIntervalSec = 0;
       serviceConfig = {
-        Restart="on-failure";
-        RestartSec="1";
         DynamicUser = true;
-        StartLimitInterval="0";
-        PrivateDevices=true;
-        AmbientCapabilities="CAP_NET_BIND_SERVICE";
-        CapabilityBoundingSet="CAP_NET_BIND_SERVICE";
-        ExecStart = "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}";
-        ProtectHome=true;
-        RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
-        LimitNOFILE="16384";
-        TasksMax="8192";
+
+        # upstream overrides for better nixos compatibility
+        ExecStartPre = [ "" "${pkgs.dnsdist}/bin/dnsdist --check-config --config ${configFile}" ];
+        ExecStart = [ "" "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}" ];
       };
     };
   };
diff --git a/nixos/modules/services/networking/epmd.nix b/nixos/modules/services/networking/epmd.nix
index 692b75e4f0865..f7cdc0fe79c04 100644
--- a/nixos/modules/services/networking/epmd.nix
+++ b/nixos/modules/services/networking/epmd.nix
@@ -53,4 +53,6 @@ in
       };
     };
   };
+
+  meta.maintainers = teams.beam.members;
 }
diff --git a/nixos/modules/services/networking/flashpolicyd.nix b/nixos/modules/services/networking/flashpolicyd.nix
deleted file mode 100644
index 7f25083307c72..0000000000000
--- a/nixos/modules/services/networking/flashpolicyd.nix
+++ /dev/null
@@ -1,85 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.flashpolicyd;
-
-  flashpolicyd = pkgs.stdenv.mkDerivation {
-    name = "flashpolicyd-0.6";
-
-    src = pkgs.fetchurl {
-      name = "flashpolicyd_v0.6.zip";
-      url = "https://download.adobe.com/pub/adobe/devnet/flashplayer/articles/socket_policy_files/flashpolicyd_v0.6.zip";
-      sha256 = "16zk237233npwfq1m4ksy4g5lzy1z9fp95w7pz0cdlpmv0fv9sm3";
-    };
-
-    buildInputs = [ pkgs.unzip pkgs.perl ];
-
-    installPhase = "mkdir $out; cp -pr * $out/; chmod +x $out/*/*.pl";
-  };
-
-  flashpolicydWrapper = pkgs.writeScriptBin "flashpolicyd"
-    ''
-      #! ${pkgs.runtimeShell}
-      exec ${flashpolicyd}/Perl_xinetd/in.flashpolicyd.pl \
-        --file=${pkgs.writeText "flashpolixy.xml" cfg.policy} \
-        2> /dev/null
-    '';
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.flashpolicyd = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description =
-          ''
-            Whether to enable the Flash Policy server.  This is
-            necessary if you want Flash applications to make
-            connections to your server.
-          '';
-      };
-
-      policy = mkOption {
-        default =
-          ''
-            <?xml version="1.0"?>
-            <!DOCTYPE cross-domain-policy SYSTEM "/xml/dtds/cross-domain-policy.dtd">
-            <cross-domain-policy>
-              <site-control permitted-cross-domain-policies="master-only"/>
-              <allow-access-from domain="*" to-ports="*" />
-            </cross-domain-policy>
-          '';
-        description = "The policy to be served.  The default is to allow connections from any domain to any port.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    services.xinetd.enable = true;
-
-    services.xinetd.services = singleton
-      { name = "flashpolicy";
-        port = 843;
-        unlisted = true;
-        server = "${flashpolicydWrapper}/bin/flashpolicyd";
-      };
-
-  };
-
-}
diff --git a/nixos/modules/services/networking/gateone.nix b/nixos/modules/services/networking/gateone.nix
index 56f2ba21a125b..3e3a3c1aa94d4 100644
--- a/nixos/modules/services/networking/gateone.nix
+++ b/nixos/modules/services/networking/gateone.nix
@@ -10,12 +10,12 @@ options = {
       pidDir = mkOption {
         default = "/run/gateone";
         type = types.path;
-        description = ''Path of pid files for GateOne.'';
+        description = "Path of pid files for GateOne.";
       };
       settingsDir = mkOption {
         default = "/var/lib/gateone";
         type = types.path;
-        description = ''Path of configuration files for GateOne.'';
+        description = "Path of configuration files for GateOne.";
       };
     };
 };
diff --git a/nixos/modules/services/networking/gogoclient.nix b/nixos/modules/services/networking/gogoclient.nix
index 99455b183144a..1205321818b97 100644
--- a/nixos/modules/services/networking/gogoclient.nix
+++ b/nixos/modules/services/networking/gogoclient.nix
@@ -28,6 +28,7 @@ in
 
       username = mkOption {
         default = "";
+        type = types.str;
         description = ''
           Your Gateway6 login name, if any.
         '';
@@ -42,6 +43,7 @@ in
       };
 
       server = mkOption {
+        type = types.str;
         default = "anonymous.freenet6.net";
         example = "broker.freenet6.net";
         description = "The Gateway6 server to be used.";
diff --git a/nixos/modules/services/networking/gvpe.nix b/nixos/modules/services/networking/gvpe.nix
index 92e87cd4640d0..b851facf1e320 100644
--- a/nixos/modules/services/networking/gvpe.nix
+++ b/nixos/modules/services/networking/gvpe.nix
@@ -3,7 +3,7 @@
 {config, pkgs, lib, ...}:
 
 let
-  inherit (lib) mkOption mkIf;
+  inherit (lib) mkOption mkIf types;
 
   cfg = config.services.gvpe;
 
@@ -46,12 +46,14 @@ in
 
       nodename = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description =''
           GVPE node name
         '';
       };
       configText = mkOption {
         default = null;
+        type = types.nullOr types.lines;
         example = ''
           tcp-port = 655
           udp-port = 655
@@ -72,6 +74,7 @@ in
       };
       configFile = mkOption {
         default = null;
+        type = types.nullOr types.path;
         example = "/root/my-gvpe-conf";
         description = ''
           GVPE config file, if already present
@@ -79,12 +82,14 @@ in
       };
       ipAddress = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           IP address to assign to GVPE interface
         '';
       };
       subnet = mkOption {
         default = null;
+        type = types.nullOr types.str;
         example = "10.0.0.0/8";
         description = ''
           IP subnet assigned to GVPE network
@@ -92,6 +97,7 @@ in
       };
       customIFSetup = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Additional commands to apply in ifup script
         '';
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 5d73038363a94..f719ff59cc7f0 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -20,8 +20,8 @@ let
     ssid=${cfg.ssid}
     hw_mode=${cfg.hwMode}
     channel=${toString cfg.channel}
-    ${optionalString (cfg.countryCode != null) ''country_code=${cfg.countryCode}''}
-    ${optionalString (cfg.countryCode != null) ''ieee80211d=1''}
+    ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
+    ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
 
     # logging (debug level)
     logger_syslog=-1
@@ -68,6 +68,7 @@ in
       interface = mkOption {
         default = "";
         example = "wlp2s0";
+        type = types.str;
         description = ''
           The interfaces <command>hostapd</command> will use.
         '';
diff --git a/nixos/modules/services/networking/hylafax/modem-default.nix b/nixos/modules/services/networking/hylafax/modem-default.nix
index 7529b5b0aafdd..707b82092829a 100644
--- a/nixos/modules/services/networking/hylafax/modem-default.nix
+++ b/nixos/modules/services/networking/hylafax/modem-default.nix
@@ -5,7 +5,7 @@
 {
 
   TagLineFont = "etc/LiberationSans-25.pcf";
-  TagLineLocale = ''en_US.UTF-8'';
+  TagLineLocale = "en_US.UTF-8";
 
   AdminGroup = "root";  # groups that can change server config
   AnswerRotary = "fax";  # don't accept anything else but faxes
@@ -16,7 +16,7 @@
   SessionTracing = "0x78701";
   UUCPLockDir = "/var/lock";
 
-  SendPageCmd = ''${pkgs.coreutils}/bin/false'';  # prevent pager transmit
-  SendUUCPCmd = ''${pkgs.coreutils}/bin/false'';  # prevent UUCP transmit
+  SendPageCmd = "${pkgs.coreutils}/bin/false";  # prevent pager transmit
+  SendUUCPCmd = "${pkgs.coreutils}/bin/false";  # prevent UUCP transmit
 
 }
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
index 9e28d09dffca5..7f18c0d39ab4d 100644
--- a/nixos/modules/services/networking/hylafax/options.nix
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -85,8 +85,8 @@ let
       # Otherwise, we use `false` to provoke
       # an error if hylafax tries to use it.
       c.sendmailPath = mkMerge [
-        (mkIfDefault noWrapper ''${pkgs.coreutils}/bin/false'')
-        (mkIfDefault (!noWrapper) ''${wrapperDir}/${program}'')
+        (mkIfDefault noWrapper "${pkgs.coreutils}/bin/false")
+        (mkIfDefault (!noWrapper) "${wrapperDir}/${program}")
       ];
       importDefaultConfig = file:
         lib.attrsets.mapAttrs
@@ -121,7 +121,7 @@ in
 
   options.services.hylafax = {
 
-    enable = mkEnableOption ''HylaFAX server'';
+    enable = mkEnableOption "HylaFAX server";
 
     autostart = mkOption {
       type = bool;
@@ -139,28 +139,28 @@ in
       type = nullOr str1;
       default = null;
       example = "49";
-      description = ''Country code for server and all modems.'';
+      description = "Country code for server and all modems.";
     };
 
     areaCode = mkOption {
       type = nullOr str1;
       default = null;
       example = "30";
-      description = ''Area code for server and all modems.'';
+      description = "Area code for server and all modems.";
     };
 
     longDistancePrefix = mkOption {
       type = nullOr str;
       default = null;
       example = "0";
-      description = ''Long distance prefix for server and all modems.'';
+      description = "Long distance prefix for server and all modems.";
     };
 
     internationalPrefix = mkOption {
       type = nullOr str;
       default = null;
       example = "00";
-      description = ''International prefix for server and all modems.'';
+      description = "International prefix for server and all modems.";
     };
 
     spoolAreaPath = mkOption {
@@ -267,7 +267,7 @@ in
     spoolExtraInit = mkOption {
       type = lines;
       default = "";
-      example = ''chmod 0755 .  # everyone may read my faxes'';
+      example = "chmod 0755 .  # everyone may read my faxes";
       description = ''
         Additional shell code that is executed within the
         spooling area directory right after its setup.
@@ -345,7 +345,7 @@ in
     faxqclean.doneqMinutes = mkOption {
       type = int1;
       default = 15;
-      example = literalExample ''24*60'';
+      example = literalExample "24*60";
       description = ''
         Set the job
         age threshold (in minutes) that controls how long
@@ -355,7 +355,7 @@ in
     faxqclean.docqMinutes = mkOption {
       type = int1;
       default = 60;
-      example = literalExample ''24*60'';
+      example = literalExample "24*60";
       description = ''
         Set the document
         age threshold (in minutes) that controls how long
diff --git a/nixos/modules/services/networking/hylafax/systemd.nix b/nixos/modules/services/networking/hylafax/systemd.nix
index b9b9b9dca4f0a..f63f7c97ad1ca 100644
--- a/nixos/modules/services/networking/hylafax/systemd.nix
+++ b/nixos/modules/services/networking/hylafax/systemd.nix
@@ -16,12 +16,12 @@ let
       mkLines = conf:
         (lib.concatLists
         (lib.flip lib.mapAttrsToList conf
-        (k: map (v: ''${k}: ${v}'')
+        (k: map (v: "${k}: ${v}")
       )));
       include = mkLines { Include = conf.Include or []; };
       other = mkLines ( conf // { Include = []; } );
     in
-      pkgs.writeText ''hylafax-config${name}''
+      pkgs.writeText "hylafax-config${name}"
       (concatStringsSep "\n" (include ++ other));
 
   globalConfigPath = mkConfigFile "" cfg.faxqConfig;
@@ -29,7 +29,7 @@ let
   modemConfigPath =
     let
       mkModemConfigFile = { config, name, ... }:
-        mkConfigFile ''.${name}''
+        mkConfigFile ".${name}"
         (cfg.commonModemConfig // config);
       mkLine = { name, type, ... }@modem: ''
         # check if modem config file exists:
@@ -81,7 +81,7 @@ let
     description = "HylaFAX queue manager sendq watch";
     documentation = [ "man:faxq(8)" "man:sendq(5)" ];
     wantedBy = [ "multi-user.target" ];
-    pathConfig.PathExistsGlob = [ ''${cfg.spoolAreaPath}/sendq/q*'' ];
+    pathConfig.PathExistsGlob = [ "${cfg.spoolAreaPath}/sendq/q*" ];
   };
 
   timers = mkMerge [
@@ -134,7 +134,7 @@ let
         exit 1
       fi
     '';
-    serviceConfig.ExecStop = ''${setupSpoolScript}'';
+    serviceConfig.ExecStop = "${setupSpoolScript}";
     serviceConfig.RemainAfterExit = true;
     serviceConfig.Type = "oneshot";
     unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ];
@@ -145,7 +145,7 @@ let
     documentation = [ "man:faxq(8)" ];
     requires = [ "hylafax-spool.service" ];
     after = [ "hylafax-spool.service" ];
-    wants = mapModems ( { name, ... }: ''hylafax-faxgetty@${name}.service'' );
+    wants = mapModems ( { name, ... }: "hylafax-faxgetty@${name}.service" );
     wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
     serviceConfig.Type = "forking";
     serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/faxq -q "${cfg.spoolAreaPath}"'';
@@ -155,7 +155,7 @@ let
     # stopped will always yield a failed send attempt:
     # The fax service is started when the job is created with
     # `sendfax`, but modems need some time to initialize.
-    serviceConfig.ExecStartPost = [ ''${waitFaxqScript}'' ];
+    serviceConfig.ExecStartPost = [ "${waitFaxqScript}" ];
     # faxquit fails if the pipe is already gone
     # (e.g. the service is already stopping)
     serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}"'';
@@ -186,7 +186,7 @@ let
     wantedBy = mkIf cfg.faxcron.enable.spoolInit requires;
     startAt = mkIf (cfg.faxcron.enable.frequency!=null) cfg.faxcron.enable.frequency;
     serviceConfig.ExecStart = concatStringsSep " " [
-      ''${pkgs.hylafaxplus}/spool/bin/faxcron''
+      "${pkgs.hylafaxplus}/spool/bin/faxcron"
       ''-q "${cfg.spoolAreaPath}"''
       ''-info ${toString cfg.faxcron.infoDays}''
       ''-log  ${toString cfg.faxcron.logDays}''
@@ -202,18 +202,18 @@ let
     wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires;
     startAt = mkIf (cfg.faxqclean.enable.frequency!=null) cfg.faxqclean.enable.frequency;
     serviceConfig.ExecStart = concatStringsSep " " [
-      ''${pkgs.hylafaxplus}/spool/bin/faxqclean''
+      "${pkgs.hylafaxplus}/spool/bin/faxqclean"
       ''-q "${cfg.spoolAreaPath}"''
-      ''-v''
-      (optionalString (cfg.faxqclean.archiving!="never") ''-a'')
-      (optionalString (cfg.faxqclean.archiving=="always")  ''-A'')
+      "-v"
+      (optionalString (cfg.faxqclean.archiving!="never") "-a")
+      (optionalString (cfg.faxqclean.archiving=="always")  "-A")
       ''-j ${toString (cfg.faxqclean.doneqMinutes*60)}''
       ''-d ${toString (cfg.faxqclean.docqMinutes*60)}''
     ];
   };
 
   mkFaxgettyService = { name, ... }:
-    lib.nameValuePair ''hylafax-faxgetty@${name}'' rec {
+    lib.nameValuePair "hylafax-faxgetty@${name}" rec {
       description = "HylaFAX faxgetty for %I";
       documentation = [ "man:faxgetty(8)" ];
       bindsTo = [ "dev-%i.device" ];
@@ -221,7 +221,7 @@ let
       after = bindsTo ++ requires;
       before = [ "hylafax-faxq.service" "getty.target" ];
       unitConfig.StopWhenUnneeded = true;
-      unitConfig.AssertFileNotEmpty = ''${cfg.spoolAreaPath}/etc/config.%I'';
+      unitConfig.AssertFileNotEmpty = "${cfg.spoolAreaPath}/etc/config.%I";
       serviceConfig.UtmpIdentifier = "%I";
       serviceConfig.TTYPath = "/dev/%I";
       serviceConfig.Restart = "always";
diff --git a/nixos/modules/services/networking/icecream/daemon.nix b/nixos/modules/services/networking/icecream/daemon.nix
new file mode 100644
index 0000000000000..2975696f9c243
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/daemon.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.daemon;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.daemon = {
+
+     enable = mkEnableOption "Icecream Daemon";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open receive port in the firewall.
+        '';
+      };
+
+      openBroadcast = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the firewall for scheduler discovery.
+        '';
+      };
+
+      cacheLimit = mkOption {
+        type = types.ints.u16;
+        default = 256;
+        description = ''
+          Maximum size in Megabytes of cache used to store compile environments of compile clients.
+        '';
+      };
+
+      netName = mkOption {
+        type = types.str;
+        default = "ICECREAM";
+        description = ''
+          Network name to connect to. A scheduler with the same name needs to be running.
+        '';
+      };
+
+      noRemote = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Prevent jobs from other nodes being scheduled on this daemon.
+        '';
+      };
+
+      schedulerHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Explicit scheduler hostname, useful in firewalled environments.
+
+          Uses scheduler autodiscovery via broadcast if set to null.
+        '';
+      };
+
+      maxProcesses = mkOption {
+        type = types.nullOr types.ints.u16;
+        default = null;
+        description = ''
+          Maximum number of compile jobs started in parallel for this daemon.
+
+          Uses the number of CPUs if set to null.
+        '';
+      };
+
+      nice = mkOption {
+        type = types.int;
+        default = 5;
+        description = ''
+          The level of niceness to use.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Hostname of the daemon in the icecream infrastructure.
+
+          Uses the hostname retrieved via uname if set to null.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "icecc";
+        description = ''
+          User to run the icecream daemon as. Set to root to enable receive of
+          remote compile environments.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters.";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 10245 ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openBroadcast [ 8765 ];
+
+    systemd.services.icecc-daemon = {
+      description = "Icecream compile daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/iceccd"
+          "-b" "$STATE_DIRECTORY"
+          "-u" "icecc"
+          (toString cfg.nice)
+        ]
+        ++ optionals (cfg.schedulerHost != null) ["-s" cfg.schedulerHost]
+        ++ optionals (cfg.netName != null) [ "-n" cfg.netName ]
+        ++ optionals (cfg.cacheLimit != null) [ "--cache-limit" (toString cfg.cacheLimit) ]
+        ++ optionals (cfg.maxProcesses != null) [ "-m" (toString cfg.maxProcesses) ]
+        ++ optionals (cfg.hostname != null) [ "-N" (cfg.hostname) ]
+        ++ optional  cfg.noRemote "--no-remote"
+        ++ cfg.extraArgs);
+        DynamicUser = true;
+        User = "icecc";
+        Group = "icecc";
+        StateDirectory = "icecc";
+        RuntimeDirectory = "icecc";
+        AmbientCapabilities = "CAP_SYS_CHROOT";
+        CapabilityBoundingSet = "CAP_SYS_CHROOT";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/icecream/scheduler.nix b/nixos/modules/services/networking/icecream/scheduler.nix
new file mode 100644
index 0000000000000..4ccbf27015d7c
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/scheduler.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.scheduler;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.scheduler = {
+      enable = mkEnableOption "Icecream Scheduler";
+
+      netName = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Network name for the icecream scheduler.
+
+          Uses the default ICECREAM if null.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8765;
+        description = ''
+          Server port to listen for icecream daemon requests.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the daemon port in the firewall.
+        '';
+      };
+
+      openTelnet = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the telnet TCP port on 8766.
+        '';
+      };
+
+      persistentClientConnection = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to prevent clients from connecting to a better scheduler.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkMerge [
+      (mkIf cfg.openFirewall [ cfg.port ])
+      (mkIf cfg.openTelnet [ 8766 ])
+    ];
+
+    systemd.services.icecc-scheduler = {
+      description = "Icecream scheduling server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/icecc-scheduler"
+          "-p" (toString cfg.port)
+        ]
+        ++ optionals (cfg.netName != null) [ "-n" (toString cfg.netName) ]
+        ++ optional cfg.persistentClientConnection "-r"
+        ++ cfg.extraArgs);
+
+        DynamicUser = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
index 91d0bf437d693..0781159b6ee73 100644
--- a/nixos/modules/services/networking/ircd-hybrid/default.nix
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -40,6 +40,7 @@ in
 
       serverName = mkOption {
         default = "hades.arpa";
+        type = types.str;
         description = "
           IRCD server name.
         ";
@@ -47,6 +48,7 @@ in
 
       sid = mkOption {
         default = "0NL";
+        type = types.str;
         description = "
           IRCD server unique ID in a net of servers.
         ";
@@ -54,6 +56,7 @@ in
 
       description = mkOption {
         default = "Hybrid-7 IRC server.";
+        type = types.str;
         description = "
           IRCD server description.
         ";
@@ -62,6 +65,7 @@ in
       rsaKey = mkOption {
         default = null;
         example = literalExample "/root/certificates/irc.key";
+        type = types.nullOr types.path;
         description = "
           IRCD server RSA key.
         ";
@@ -70,6 +74,7 @@ in
       certificate = mkOption {
         default = null;
         example = literalExample "/root/certificates/irc.pem";
+        type = types.nullOr types.path;
         description = "
           IRCD server SSL certificate. There are some limitations - read manual.
         ";
@@ -77,6 +82,7 @@ in
 
       adminEmail = mkOption {
         default = "<bit-bucket@example.com>";
+        type = types.str;
         example = "<name@domain.tld>";
         description = "
           IRCD server administrator e-mail.
@@ -86,6 +92,7 @@ in
       extraIPs = mkOption {
         default = [];
         example = ["127.0.0.1"];
+        type = types.listOf types.str;
         description = "
           Extra IP's to bind.
         ";
@@ -93,6 +100,7 @@ in
 
       extraPort = mkOption {
         default = "7117";
+        type = types.str;
         description = "
           Extra port to avoid filtering.
         ";
diff --git a/nixos/modules/services/networking/iwd.nix b/nixos/modules/services/networking/iwd.nix
index 6be67a8b96f4d..99e5e78badd28 100644
--- a/nixos/modules/services/networking/iwd.nix
+++ b/nixos/modules/services/networking/iwd.nix
@@ -22,6 +22,11 @@ in {
 
     systemd.packages = [ pkgs.iwd ];
 
+    systemd.network.links."80-iwd" = {
+      matchConfig.Type = "wlan";
+      linkConfig.NamePolicy = "keep kernel";
+    };
+
     systemd.services.iwd.wantedBy = [ "multi-user.target" ];
   };
 
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index 5482e997a4018..80f35d56e2dbf 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -191,6 +191,16 @@ in
         Whether to open ports in the firewall for the videobridge.
       '';
     };
+
+    apis = mkOption {
+      type = with types; listOf str;
+      description = ''
+        What is passed as --apis= parameter. If this is empty, "none" is passed.
+        Needed for monitoring jitsi.
+      '';
+      default = [];
+      example = literalExample "[ \"colibri\" \"rest\" ]";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -221,7 +231,7 @@ in
         "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
       ) cfg.xmppConfigs))
       + ''
-        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=none
+        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=${if (cfg.apis == []) then "none" else concatStringsSep "," cfg.apis}
       '';
 
       serviceConfig = {
diff --git a/nixos/modules/services/networking/kippo.nix b/nixos/modules/services/networking/kippo.nix
index 553415a2f3297..6fedb0a270f4c 100644
--- a/nixos/modules/services/networking/kippo.nix
+++ b/nixos/modules/services/networking/kippo.nix
@@ -17,37 +17,37 @@ in
       enable = mkOption {
         default = false;
         type = types.bool;
-        description = ''Enable the kippo honeypot ssh server.'';
+        description = "Enable the kippo honeypot ssh server.";
       };
       port = mkOption {
         default = 2222;
         type = types.int;
-        description = ''TCP port number for kippo to bind to.'';
+        description = "TCP port number for kippo to bind to.";
       };
       hostname = mkOption {
         default = "nas3";
         type = types.str;
-        description = ''Hostname for kippo to present to SSH login'';
+        description = "Hostname for kippo to present to SSH login";
       };
       varPath = mkOption {
         default = "/var/lib/kippo";
         type = types.path;
-        description = ''Path of read/write files needed for operation and configuration.'';
+        description = "Path of read/write files needed for operation and configuration.";
       };
       logPath = mkOption {
         default = "/var/log/kippo";
         type = types.path;
-        description = ''Path of log files needed for operation and configuration.'';
+        description = "Path of log files needed for operation and configuration.";
       };
       pidPath = mkOption {
         default = "/run/kippo";
         type = types.path;
-        description = ''Path of pid files needed for operation.'';
+        description = "Path of pid files needed for operation.";
       };
       extraConfig = mkOption {
         default = "";
         type = types.lines;
-        description = ''Extra verbatim configuration added to the end of kippo.cfg.'';
+        description = "Extra verbatim configuration added to the end of kippo.cfg.";
       };
     };
 
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index ccb34163d5f36..4131ff8be5d02 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -23,18 +23,14 @@ let
       '';
 
   configFile = pkgs.writeText "kresd.conf" (
-    optionalString (cfg.listenDoH != []) ''
-      modules.load('http')
-    ''
+    ""
     + concatMapStrings (mkListen "dns") cfg.listenPlain
     + concatMapStrings (mkListen "tls") cfg.listenTLS
-    + concatMapStrings (mkListen "doh") cfg.listenDoH
+    + concatMapStrings (mkListen "doh2") cfg.listenDoH
     + cfg.extraConfig
   );
 
-  package = if cfg.listenDoH == []
-    then pkgs.knot-resolver # never force `extraFeatures = false`
-    else pkgs.knot-resolver.override { extraFeatures = true; };
+  package = pkgs.knot-resolver;
 in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
@@ -92,7 +88,7 @@ in {
       default = [];
       example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
       description = ''
-        Addresses and ports on which kresd should provide DNS over HTTPS (see RFC 8484).
+        Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
         For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
@@ -139,10 +135,12 @@ in {
       CacheDirectory = "knot-resolver";
       CacheDirectoryMode = "0770";
     };
+    # We don't mind running stop phase from wrong version.  It seems less racy.
+    systemd.services."kresd@".stopIfChanged = false;
 
     # 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
+    # TODO: remove later, probably between 20.09 and 21.05
     systemd.tmpfiles.rules = [ "R /var/cache/kresd" ];
   };
 }
diff --git a/nixos/modules/services/networking/mailpile.nix b/nixos/modules/services/networking/mailpile.nix
index b79ee11d17db2..4673a2580b602 100644
--- a/nixos/modules/services/networking/mailpile.nix
+++ b/nixos/modules/services/networking/mailpile.nix
@@ -21,11 +21,13 @@ in
       enable = mkEnableOption "Mailpile the mail client";
 
       hostname = mkOption {
+        type = types.str;
         default = "localhost";
         description = "Listen to this hostname or ip.";
       };
       port = mkOption {
-        default = "33411";
+        type = types.port;
+        default = 33411;
         description = "Listen on this port.";
       };
     };
diff --git a/nixos/modules/services/networking/morty.nix b/nixos/modules/services/networking/morty.nix
index e3a6444c11635..e110a5c86101f 100644
--- a/nixos/modules/services/networking/morty.nix
+++ b/nixos/modules/services/networking/morty.nix
@@ -29,9 +29,11 @@ in
       key = mkOption {
         type = types.str;
         default = "";
-        description = "HMAC url validation key (hexadecimal encoded).
-	Leave blank to disable. Without validation key, anyone can
-	submit proxy requests. Leave blank to disable.";
+        description = ''
+          HMAC url validation key (hexadecimal encoded).
+          Leave blank to disable. Without validation key, anyone can
+          submit proxy requests. Leave blank to disable.
+        '';
         defaultText = "No HMAC url validation. Generate with echo -n somevalue | openssl dgst -sha1 -hmac somekey";
       };
 
@@ -85,10 +87,10 @@ in
         serviceConfig = {
           User = "morty";
           ExecStart = ''${cfg.package}/bin/morty              \
-	    -listen ${cfg.listenAddress}:${toString cfg.port} \
-	    ${optionalString cfg.ipv6 "-ipv6"}                \
-	    ${optionalString (cfg.key != "") "-key " + cfg.key} \
-	  '';
+            -listen ${cfg.listenAddress}:${toString cfg.port} \
+            ${optionalString cfg.ipv6 "-ipv6"}                \
+            ${optionalString (cfg.key != "") "-key " + cfg.key} \
+          '';
         };
       };
     environment.systemPackages = [ cfg.package ];
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index d2feb93e2b723..10b49d9b2206e 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -123,12 +123,33 @@ in
               '';
             };
 
+            passwordFile = mkOption {
+              type = with types; uniq (nullOr str);
+              example = "/path/to/file";
+              default = null;
+              description = ''
+                Specifies the path to a file containing the
+                clear text password for the MQTT user.
+              '';
+            };
+
             hashedPassword = mkOption {
               type = with types; uniq (nullOr str);
               default = null;
               description = ''
                 Specifies the hashed password for the MQTT User.
-                <option>hashedPassword</option> overrides <option>password</option>.
+                To generate hashed password install <literal>mosquitto</literal>
+                package and use <literal>mosquitto_passwd</literal>.
+              '';
+            };
+
+            hashedPasswordFile = mkOption {
+              type = with types; uniq (nullOr str);
+              example = "/path/to/file";
+              default = null;
+              description = ''
+                Specifies the path to a file containing the
+                hashed password for the MQTT user.
                 To generate hashed password install <literal>mosquitto</literal>
                 package and use <literal>mosquitto_passwd</literal>.
               '';
@@ -190,6 +211,13 @@ in
 
   config = mkIf cfg.enable {
 
+    assertions = mapAttrsToList (name: cfg: {
+      assertion = length (filter (s: s != null) (with cfg; [
+        password passwordFile hashedPassword hashedPasswordFile
+      ])) <= 1;
+      message = "Cannot set more than one password option";
+    }) cfg.users;
+
     systemd.services.mosquitto = {
       description = "Mosquitto MQTT Broker Daemon";
       wantedBy = [ "multi-user.target" ];
@@ -204,13 +232,27 @@ in
         Restart = "on-failure";
         ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ReadWritePaths = "${cfg.dataDir}";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        NoNewPrivileges = true;
       };
       preStart = ''
         rm -f ${cfg.dataDir}/passwd
         touch ${cfg.dataDir}/passwd
       '' + concatStringsSep "\n" (
         mapAttrsToList (n: c:
-          if c.hashedPassword != null then
+          if c.hashedPasswordFile != null then
+            "echo '${n}:'$(cat '${c.hashedPasswordFile}') >> ${cfg.dataDir}/passwd"
+          else if c.passwordFile != null then
+            "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} $(cat '${c.passwordFile}')"
+          else if c.hashedPassword != null then
             "echo '${n}:${c.hashedPassword}' >> ${cfg.dataDir}/passwd"
           else optionalString (c.password != null)
             "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} '${c.password}'"
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
index cc98414257ca7..6f595ca4be2b2 100644
--- a/nixos/modules/services/networking/mullvad-vpn.nix
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -15,6 +15,9 @@ with lib;
   config = mkIf cfg.enable {
     boot.kernelModules = [ "tun" ];
 
+    # mullvad-daemon writes to /etc/iproute2/rt_tables
+    networking.iproute2.enable = true;
+
     systemd.services.mullvad-daemon = {
       description = "Mullvad VPN daemon";
       wantedBy = [ "multi-user.target" ];
@@ -29,9 +32,9 @@ with lib;
         # Needed for ping
         "/run/wrappers"
       ];
+      startLimitBurst = 5;
+      startLimitIntervalSec = 20;
       serviceConfig = {
-        StartLimitBurst = 5;
-        StartLimitIntervalSec = 20;
         ExecStart = "${pkgs.mullvad-vpn}/bin/mullvad-daemon -v --disable-stdout-timestamps";
         Restart = "always";
         RestartSec = 1;
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index b384f436861db..b03630208df83 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -109,6 +109,13 @@ in
         description = "Host to bind to. Defaults binding on all addresses.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.murmur;
+        defaultText = "pkgs.murmur";
+        description = "Overridable attribute of the murmur package to use.";
+      };
+
       password = mkOption {
         type = types.str;
         default = "";
@@ -278,6 +285,10 @@ in
       home            = "/var/lib/murmur";
       createHome      = true;
       uid             = config.ids.uids.murmur;
+      group           = "murmur";
+    };
+    users.groups.murmur = {
+      gid             = config.ids.gids.murmur;
     };
 
     systemd.services.murmur = {
@@ -295,11 +306,12 @@ in
         Type = if forking then "forking" else "simple";
         PIDFile = mkIf forking "/run/murmur/murmurd.pid";
         EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
-        ExecStart = "${pkgs.murmur}/bin/murmurd -ini /run/murmur/murmurd.ini";
+        ExecStart = "${cfg.package}/bin/murmurd -ini /run/murmur/murmurd.ini";
         Restart = "always";
         RuntimeDirectory = "murmur";
         RuntimeDirectoryMode = "0700";
         User = "murmur";
+        Group = "murmur";
       };
     };
   };
diff --git a/nixos/modules/services/networking/namecoind.nix b/nixos/modules/services/networking/namecoind.nix
index 16f85df2e77c8..4966ed2cac8dc 100644
--- a/nixos/modules/services/networking/namecoind.nix
+++ b/nixos/modules/services/networking/namecoind.nix
@@ -165,6 +165,8 @@ in
       after    = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      startLimitIntervalSec = 120;
+      startLimitBurst = 5;
       serviceConfig = {
         User  = "namecoin";
         Group = "namecoin";
@@ -176,8 +178,6 @@ in
         TimeoutStopSec     = "60s";
         TimeoutStartSec    = "2s";
         Restart            = "always";
-        StartLimitInterval = "120s";
-        StartLimitBurst    = "5";
       };
 
       preStart = optionalString (cfg.wallet != "${dataDir}/wallet.dat")  ''
diff --git a/nixos/modules/services/networking/nar-serve.nix b/nixos/modules/services/networking/nar-serve.nix
new file mode 100644
index 0000000000000..ddd42fa010737
--- /dev/null
+++ b/nixos/modules/services/networking/nar-serve.nix
@@ -0,0 +1,55 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.nar-serve;
+in
+{
+  meta = {
+    maintainers = [ maintainers.rizary ];
+  };
+  options = {
+    services.nar-serve = {
+      enable = mkEnableOption "Serve NAR file contents via HTTP";
+
+      port = mkOption {
+        type = types.int;
+        default = 8383;
+        description = ''
+          Port number where nar-serve will listen on.
+        '';
+      };
+
+      cacheURL = mkOption {
+        type = types.str;
+        default = "https://cache.nixos.org/";
+        description = ''
+          Binary cache URL to connect to.
+
+          The URL format is compatible with the nix remote url style, such as:
+          - http://, https:// for binary caches via HTTP or HTTPS
+          - s3:// for binary caches stored in Amazon S3
+          - gs:// for binary caches stored in Google Cloud Storage
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nar-serve = {
+      description = "NAR server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment.PORT = toString cfg.port;
+      environment.NAR_CACHE_URL = cfg.cacheURL;
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        ExecStart = "${pkgs.nar-serve}/bin/nar-serve";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 21ae9eb8b6d46..45eb500fe8ce9 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -9,7 +9,14 @@ with lib;
 let
   cfg = config.networking.nat;
 
-  dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}";
+  mkDest = externalIP: if externalIP == null
+                       then "-j MASQUERADE"
+                       else "-j SNAT --to-source ${externalIP}";
+  dest = mkDest cfg.externalIP;
+  destIPv6 = mkDest cfg.externalIPv6;
+
+  # Whether given IP (plus optional port) is an IPv6.
+  isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
 
   helpers = import ./helpers.nix { inherit config lib; };
 
@@ -28,63 +35,80 @@ let
     ${cfg.extraStopCommands}
   '';
 
-  setupNat = ''
-    ${helpers}
-    # Create subchain where we store rules
-    ip46tables -w -t nat -N nixos-nat-pre
-    ip46tables -w -t nat -N nixos-nat-post
-    ip46tables -w -t nat -N nixos-nat-out
-
+  mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
     # We can't match on incoming interface in POSTROUTING, so
     # mark packets coming from the internal interfaces.
     ${concatMapStrings (iface: ''
-      iptables -w -t nat -A nixos-nat-pre \
+      ${iptables} -w -t nat -A nixos-nat-pre \
         -i '${iface}' -j MARK --set-mark 1
     '') cfg.internalInterfaces}
 
     # NAT the marked packets.
     ${optionalString (cfg.internalInterfaces != []) ''
-      iptables -w -t nat -A nixos-nat-post -m mark --mark 1 \
+      ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
         ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
     ''}
 
     # NAT packets coming from the internal IPs.
     ${concatMapStrings (range: ''
-      iptables -w -t nat -A nixos-nat-post \
+      ${iptables} -w -t nat -A nixos-nat-post \
         -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-    '') cfg.internalIPs}
+    '') internalIPs}
 
     # NAT from external ports to internal ports.
     ${concatMapStrings (fwd: ''
-      iptables -w -t nat -A nixos-nat-pre \
+      ${iptables} -w -t nat -A nixos-nat-pre \
         -i ${toString cfg.externalInterface} -p ${fwd.proto} \
         --dport ${builtins.toString fwd.sourcePort} \
         -j DNAT --to-destination ${fwd.destination}
 
       ${concatMapStrings (loopbackip:
         let
-          m                = builtins.match "([0-9.]+):([0-9-]+)" fwd.destination;
-          destinationIP    = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
-          destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
+          matchIP          = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+          m                = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
+          destinationIP    = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
+          destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
         in ''
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-          iptables -w -t nat -A nixos-nat-out \
+          ${iptables} -w -t nat -A nixos-nat-out \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
 
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
-          iptables -w -t nat -A nixos-nat-pre \
+          ${iptables} -w -t nat -A nixos-nat-pre \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
 
-          iptables -w -t nat -A nixos-nat-post \
+          ${iptables} -w -t nat -A nixos-nat-post \
             -d ${destinationIP} -p ${fwd.proto} \
             --dport ${destinationPorts} \
             -j SNAT --to-source ${loopbackip}
         '') fwd.loopbackIPs}
-    '') cfg.forwardPorts}
+    '') forwardPorts}
+  '';
+
+  setupNat = ''
+    ${helpers}
+    # Create subchains where we store rules
+    ip46tables -w -t nat -N nixos-nat-pre
+    ip46tables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-out
+
+    ${mkSetupNat {
+      iptables = "iptables";
+      inherit dest;
+      inherit (cfg) internalIPs;
+      forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+    }}
+
+    ${optionalString cfg.enableIPv6 (mkSetupNat {
+      iptables = "ip6tables";
+      dest = destIPv6;
+      internalIPs = cfg.internalIPv6s;
+      forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+    })}
 
     ${optionalString (cfg.dmzHost != null) ''
       iptables -w -t nat -A nixos-nat-pre \
@@ -117,6 +141,15 @@ in
         '';
     };
 
+    networking.nat.enableIPv6 = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable IPv6 NAT.
+        '';
+    };
+
     networking.nat.internalInterfaces = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -141,6 +174,18 @@ in
         '';
     };
 
+    networking.nat.internalIPv6s = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "fc00::/64" ];
+      description =
+        ''
+          The IPv6 address ranges for which to perform NAT.  Packets
+          coming from these addresses (on any interface) and destined
+          for the external interface will be rewritten.
+        '';
+    };
+
     networking.nat.externalInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -164,6 +209,19 @@ in
         '';
     };
 
+    networking.nat.externalIPv6 = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "2001:dc0:2001:11::175";
+      description =
+        ''
+          The public IPv6 address to which packets from the local
+          network are to be rewritten.  If this is left empty, the
+          IP address associated with the external interface will be
+          used.
+        '';
+    };
+
     networking.nat.forwardPorts = mkOption {
       type = with types; listOf (submodule {
         options = {
@@ -176,7 +234,7 @@ in
           destination = mkOption {
             type = types.str;
             example = "10.0.0.1:80";
-            description = "Forward connection to destination ip:port; to specify a port range, use ip:start-end";
+            description = "Forward connection to destination ip:port (or [ipv6]:port); to specify a port range, use ip:start-end";
           };
 
           proto = mkOption {
@@ -195,11 +253,15 @@ in
         };
       });
       default = [];
-      example = [ { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } ];
+      example = [
+        { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
+        { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
+      ];
       description =
         ''
           List of forwarded ports from the external interface to
-          internal destinations by using DNAT.
+          internal destinations by using DNAT. Destination can be
+          IPv6 if IPv6 NAT is enabled.
         '';
     };
 
@@ -246,6 +308,9 @@ in
     (mkIf config.networking.nat.enable {
 
       assertions = [
+        { assertion = cfg.enableIPv6           -> config.networking.enableIPv6;
+          message = "networking.nat.enableIPv6 requires networking.enableIPv6";
+        }
         { assertion = (cfg.dmzHost != null)    -> (cfg.externalInterface != null);
           message = "networking.nat.dmzHost requires networking.nat.externalInterface";
         }
@@ -261,6 +326,15 @@ in
         kernel.sysctl = {
           "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
           "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
+        } // optionalAttrs cfg.enableIPv6 {
+          # Do not prevent IPv6 autoconfiguration.
+          # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
+          "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
+          "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
+
+          # Forward IPv6 packets.
+          "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
+          "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
         };
       };
 
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 17c549d42c329..2e680544ec245 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -15,6 +15,7 @@ let
     networkmanager-openconnect
     networkmanager-openvpn
     networkmanager-vpnc
+    networkmanager-sstp
    ] ++ optional (!delegateWireless && !enableIwd) wpa_supplicant;
 
   delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [];
@@ -386,6 +387,9 @@ in {
 
       "NetworkManager/VPN/nm-iodine-service.name".source =
         "${networkmanager-iodine}/lib/NetworkManager/VPN/nm-iodine-service.name";
+
+      "NetworkManager/VPN/nm-sstp-service.name".source =
+        "${networkmanager-sstp}/lib/NetworkManager/VPN/nm-sstp-service.name";
       }
       // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
          {
@@ -461,7 +465,7 @@ in {
       restartTriggers = [ configFile overrideNameserversScript ];
 
       # useful binaries for user-specified hooks
-      path = [ pkgs.iproute pkgs.utillinux pkgs.coreutils ];
+      path = [ pkgs.iproute pkgs.util-linux pkgs.coreutils ];
       aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
     };
 
diff --git a/nixos/modules/services/networking/nextdns.nix b/nixos/modules/services/networking/nextdns.nix
index a633bff62ec7e..b070eeec894fe 100644
--- a/nixos/modules/services/networking/nextdns.nix
+++ b/nixos/modules/services/networking/nextdns.nix
@@ -28,9 +28,9 @@ in {
       environment = {
         SERVICE_RUN_MODE = "1";
       };
+      startLimitIntervalSec = 5;
+      startLimitBurst = 10;
       serviceConfig = {
-        StartLimitInterval = 5;
-        StartLimitBurst = 10;
         ExecStart = "${pkgs.nextdns}/bin/nextdns run ${escapeShellArgs config.services.nextdns.arguments}";
         RestartSec = 120;
         LimitMEMLOCK = "infinity";
diff --git a/nixos/modules/services/networking/nix-store-gcs-proxy.nix b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
index 3f2ce5bca4da4..0012302db2e3c 100644
--- a/nixos/modules/services/networking/nix-store-gcs-proxy.nix
+++ b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
@@ -42,9 +42,9 @@ in
       description = "A HTTP nix store that proxies requests to Google Storage";
       wantedBy = ["multi-user.target"];
 
+      startLimitIntervalSec = 10;
       serviceConfig = {
         RestartSec = 5;
-        StartLimitInterval = 10;
         ExecStart = ''
           ${pkgs.nix-store-gcs-proxy}/bin/nix-store-gcs-proxy \
             --bucket-name ${cfg.bucketName} \
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
new file mode 100644
index 0000000000000..9f1b443b89bc0
--- /dev/null
+++ b/nixos/modules/services/networking/nomad.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.nomad;
+  format = pkgs.formats.json { };
+in
+{
+  ##### interface
+  options = {
+    services.nomad = {
+      enable = mkEnableOption "Nomad, a distributed, highly available, datacenter-aware scheduler";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nomad;
+        defaultText = "pkgs.nomad";
+        description = ''
+          The package used for the Nomad agent and CLI.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = ''
+          Extra packages to add to <envar>PATH</envar> for the Nomad agent process.
+        '';
+        example = literalExample ''
+          with pkgs; [ cni-plugins ]
+        '';
+      };
+
+      dropPrivileges = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the nomad agent should be run as a non-root nomad user.
+        '';
+      };
+
+      enableDocker = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable Docker support. Needed for Nomad's docker driver.
+
+          Note that the docker group membership is effectively equivalent
+          to being root, see https://github.com/moby/moby/issues/9976.
+        '';
+      };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Additional settings paths used to configure nomad. These can be files or directories.
+        '';
+        example = literalExample ''
+          [ "/etc/nomad-mutable.json" "/run/keys/nomad-with-secrets.json" "/etc/nomad/config.d" ]
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Configuration for Nomad. See the <link xlink:href="https://www.nomadproject.io/docs/configuration">documentation</link>
+          for supported values.
+
+          Notes about <literal>data_dir</literal>:
+
+          If <literal>data_dir</literal> is set to a value other than the
+          default value of <literal>"/var/lib/nomad"</literal> it is the Nomad
+          cluster manager's responsibility to make sure that this directory
+          exists and has the appropriate permissions.
+
+          Additionally, if <literal>dropPrivileges</literal> is
+          <literal>true</literal> then <literal>data_dir</literal>
+          <emphasis>cannot</emphasis> be customized. Setting
+          <literal>dropPrivileges</literal> to <literal>true</literal> enables
+          the <literal>DynamicUser</literal> feature of systemd which directly
+          manages and operates on <literal>StateDirectory</literal>.
+        '';
+        example = literalExample ''
+          {
+            # A minimal config example:
+            server = {
+              enabled = true;
+              bootstrap_expect = 1; # for demo; no fault tolerance
+            };
+            client = {
+              enabled = true;
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    services.nomad.settings = {
+      # Agrees with `StateDirectory = "nomad"` set below.
+      data_dir = mkDefault "/var/lib/nomad";
+    };
+
+    environment = {
+      etc."nomad.json".source = format.generate "nomad.json" cfg.settings;
+      systemPackages = [ cfg.package ];
+    };
+
+    systemd.services.nomad = {
+      description = "Nomad";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      restartTriggers = [ config.environment.etc."nomad.json".source ];
+
+      path = cfg.extraPackages ++ (with pkgs; [
+        # Client mode requires at least the following:
+        coreutils
+        iproute
+        iptables
+      ]);
+
+      serviceConfig = mkMerge [
+        {
+          DynamicUser = cfg.dropPrivileges;
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
+            concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths;
+          KillMode = "process";
+          KillSignal = "SIGINT";
+          LimitNOFILE = 65536;
+          LimitNPROC = "infinity";
+          OOMScoreAdjust = -1000;
+          Restart = "on-failure";
+          RestartSec = 2;
+          TasksMax = "infinity";
+        }
+        (mkIf cfg.enableDocker {
+          SupplementaryGroups = "docker"; # space-separated string
+        })
+        (mkIf (cfg.settings.data_dir == "/var/lib/nomad") {
+          StateDirectory = "nomad";
+        })
+      ];
+
+      unitConfig = {
+        StartLimitIntervalSec = 10;
+        StartLimitBurst = 3;
+      };
+    };
+
+    assertions = [
+      {
+        assertion = cfg.dropPrivileges -> cfg.settings.data_dir == "/var/lib/nomad";
+        message = "settings.data_dir must be equal to \"/var/lib/nomad\" if dropPrivileges is true";
+      }
+    ];
+
+    # Docker support requires the Docker daemon to be running.
+    virtualisation.docker.enable = mkIf cfg.enableDocker true;
+  };
+}
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index 3ecbd06ee416b..f33c350a257a9 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -916,14 +916,14 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      startLimitBurst = 4;
+      startLimitIntervalSec = 5 * 60;  # 5 mins
       serviceConfig = {
         ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
         StandardError = "null";
         PIDFile = pidFile;
         Restart = "always";
         RestartSec = "4s";
-        StartLimitBurst = 4;
-        StartLimitInterval = "5min";
       };
 
       preStart = ''
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index 78de50583f348..96c6444c23a12 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -4,20 +4,23 @@ with lib;
 
 let
   cfg = config.services.chrony;
+  chronyPkg = cfg.package;
 
-  stateDir = "/var/lib/chrony";
+  stateDir = cfg.directory;
+  driftFile = "${stateDir}/chrony.drift";
   keyFile = "${stateDir}/chrony.keys";
 
   configFile = pkgs.writeText "chrony.conf" ''
-    ${concatMapStringsSep "\n" (server: "server " + server + " iburst") cfg.servers}
+    ${concatMapStringsSep "\n" (server: "server " + server + " " + cfg.serverOption + optionalString (cfg.enableNTS) " nts") cfg.servers}
 
     ${optionalString
       (cfg.initstepslew.enabled && (cfg.servers != []))
       "initstepslew ${toString cfg.initstepslew.threshold} ${concatStringsSep " " cfg.servers}"
     }
 
-    driftfile ${stateDir}/chrony.drift
+    driftfile ${driftFile}
     keyfile ${keyFile}
+    ${optionalString (cfg.enableNTS) "ntsdumpdir ${stateDir}"}
 
     ${optionalString (!config.time.hardwareClockInLocalTime) "rtconutc"}
 
@@ -38,14 +41,48 @@ in
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.chrony;
+        defaultText = "pkgs.chrony";
+        description = ''
+          Which chrony package to use.
+        '';
+      };
+
       servers = mkOption {
         default = config.networking.timeServers;
+        type = types.listOf types.str;
         description = ''
           The set of NTP servers from which to synchronise.
         '';
       };
 
+      serverOption = mkOption {
+        default = "iburst";
+        type = types.enum [ "iburst" "offline" ];
+        description = ''
+          Set option for server directives.
+
+          Use "iburst" to rapidly poll on startup. Recommended if your machine
+          is consistently online.
+
+          Use "offline" to prevent polling on startup. Recommended if your
+          machine boots offline or is otherwise frequently offline.
+        '';
+      };
+
+      enableNTS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Network Time Security authentication.
+          Make sure it is supported by your selected NTP server(s).
+        '';
+      };
+
       initstepslew = mkOption {
+        type = types.attrsOf (types.either types.bool types.int);
         default = {
           enabled = true;
           threshold = 1000; # by default, same threshold as 'ntpd -g' (1000s)
@@ -57,6 +94,12 @@ in
         '';
       };
 
+      directory = mkOption {
+        type = types.str;
+        default = "/var/lib/chrony";
+        description = "Directory where chrony state is stored.";
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -78,7 +121,7 @@ in
   config = mkIf cfg.enable {
     meta.maintainers = with lib.maintainers; [ thoughtpolice ];
 
-    environment.systemPackages = [ pkgs.chrony ];
+    environment.systemPackages = [ chronyPkg ];
 
     users.groups.chrony.gid = config.ids.gids.chrony;
 
@@ -95,6 +138,7 @@ in
 
     systemd.tmpfiles.rules = [
       "d ${stateDir} 0755 chrony chrony - -"
+      "f ${driftFile} 0640 chrony chrony -"
       "f ${keyFile} 0640 chrony chrony -"
     ];
 
@@ -107,12 +151,12 @@ in
         after    = [ "network.target" ];
         conflicts = [ "ntpd.service" "systemd-timesyncd.service" ];
 
-        path = [ pkgs.chrony ];
+        path = [ chronyPkg ];
 
         unitConfig.ConditionCapability = "CAP_SYS_TIME";
         serviceConfig =
           { Type = "simple";
-            ExecStart = "${pkgs.chrony}/bin/chronyd ${chronyFlags}";
+            ExecStart = "${chronyPkg}/bin/chronyd ${chronyFlags}";
 
             ProtectHome = "yes";
             ProtectSystem = "full";
diff --git a/nixos/modules/services/networking/ntp/ntpd.nix b/nixos/modules/services/networking/ntp/ntpd.nix
index 51398851adc6d..861b0db01a489 100644
--- a/nixos/modules/services/networking/ntp/ntpd.nix
+++ b/nixos/modules/services/networking/ntp/ntpd.nix
@@ -79,6 +79,7 @@ in
 
       servers = mkOption {
         default = config.networking.timeServers;
+        type = types.listOf types.str;
         description = ''
           The set of NTP servers from which to synchronise.
         '';
diff --git a/nixos/modules/services/networking/owamp.nix b/nixos/modules/services/networking/owamp.nix
index 637ed618b8938..baf64347b099d 100644
--- a/nixos/modules/services/networking/owamp.nix
+++ b/nixos/modules/services/networking/owamp.nix
@@ -10,7 +10,7 @@ in
   ###### interface
 
   options = {
-    services.owamp.enable = mkEnableOption ''Enable OWAMP server'';
+    services.owamp.enable = mkEnableOption "Enable OWAMP server";
   };
 
 
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
index 6ff181377fccd..a326eccfd65d1 100644
--- a/nixos/modules/services/networking/pdns-recursor.nix
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -3,9 +3,6 @@
 with lib;
 
 let
-  dataDir  = "/var/lib/pdns-recursor";
-  username = "pdns-recursor";
-
   cfg = config.services.pdns-recursor;
 
   oneOrMore  = type: with types; either type (listOf type);
@@ -21,7 +18,7 @@ let
     else if builtins.isList val then (concatMapStringsSep "," serialize val)
     else "";
 
-  configFile = pkgs.writeText "recursor.conf"
+  configDir = pkgs.writeTextDir "recursor.conf"
     (concatStringsSep "\n"
       (flip mapAttrsToList cfg.settings
         (name: val: "${name}=${serialize val}")));
@@ -173,45 +170,30 @@ in {
       serve-rfc1918    = cfg.serveRFC1918;
       lua-config-file  = pkgs.writeText "recursor.lua" cfg.luaConfig;
 
+      daemon         = false;
+      write-pid      = false;
       log-timestamp  = false;
       disable-syslog = true;
     };
 
-    users.users.${username} = {
-      home = dataDir;
-      createHome = true;
-      uid = config.ids.uids.pdns-recursor;
-      description = "PowerDNS Recursor daemon user";
-    };
+    systemd.packages = [ pkgs.pdns-recursor ];
 
     systemd.services.pdns-recursor = {
-      unitConfig.Documentation = "man:pdns_recursor(1) man:rec_control(1)";
-      description = "PowerDNS recursive server";
       wantedBy = [ "multi-user.target" ];
-      after    = [ "network.target" ];
 
       serviceConfig = {
-        User = username;
-        Restart    ="on-failure";
-        RestartSec = "5";
-        PrivateTmp = true;
-        PrivateDevices = true;
-        AmbientCapabilities = "cap_net_bind_service";
-        ExecStart = ''${pkgs.pdns-recursor}/bin/pdns_recursor \
-          --config-dir=${dataDir} \
-          --socket-dir=${dataDir}
-        '';
+        ExecStart = [ "" "${pkgs.pdns-recursor}/bin/pdns_recursor --config-dir=${configDir}" ];
       };
+    };
 
-      preStart = ''
-        # Link configuration file into recursor home directory
-        configPath=${dataDir}/recursor.conf
-        if [ "$(realpath $configPath)" != "${configFile}" ]; then
-          rm -f $configPath
-          ln -s ${configFile} $configPath
-        fi
-      '';
+    users.users.pdns-recursor = {
+      isSystemUser = true;
+      group = "pdns-recursor";
+      description = "PowerDNS Recursor daemon user";
     };
+
+    users.groups.pdns-recursor = {};
+
   };
 
   imports = [
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
new file mode 100644
index 0000000000000..9b2bf9f61244c
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -0,0 +1,140 @@
+{ config, options, lib, pkgs, stdenv, ... }:
+let
+  cfg = config.services.pleroma;
+in {
+  options = {
+    services.pleroma = with lib; {
+      enable = mkEnableOption "pleroma";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pleroma-otp;
+        description = "Pleroma package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "User account under which pleroma runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "Group account under which pleroma runs.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma";
+        readOnly = true;
+        description = "Directory where the pleroma service will save the uploads and static files.";
+      };
+
+      configs = mkOption {
+        type = with types; listOf str;
+        description = ''
+          Pleroma public configuration.
+
+          This list gets appended from left to
+          right into /etc/pleroma/config.exs. Elixir evaluates its
+          configuration imperatively, meaning you can override a
+          setting by appending a new str to this NixOS option list.
+
+          <emphasis>DO NOT STORE ANY PLEROMA SECRET
+          HERE</emphasis>, use
+          <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>
+          instead.
+
+          This setting is going to be stored in a file part of
+          the Nix store. The Nix store being world-readable, it's not
+          the right place to store any secret
+
+          Have a look to Pleroma section in the NixOS manual for more
+          informations.
+          '';
+      };
+
+      secretConfigFile = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma/secrets.exs";
+        description = ''
+          Path to the file containing your secret pleroma configuration.
+
+          <emphasis>DO NOT POINT THIS OPTION TO THE NIX
+          STORE</emphasis>, the store being world-readable, it'll
+          compromise all your secrets.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users = {
+      users."${cfg.user}" = {
+        description = "Pleroma user";
+        home = cfg.stateDir;
+        extraGroups = [ cfg.group ];
+      };
+      groups."${cfg.group}" = {};
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."/pleroma/config.exs".text = ''
+      ${lib.concatMapStrings (x: "${x}") cfg.configs}
+
+      # The lau/tzdata library is trying to download the latest
+      # timezone database in the OTP priv directory by default.
+      # This directory being in the store, it's read-only.
+      # Setting that up to a more appropriate location.
+      config :tzdata, :data_dir, "/var/lib/pleroma/elixir_tzdata_data"
+
+      import_config "${cfg.secretConfigFile}"
+    '';
+
+    systemd.services.pleroma = {
+      description = "Pleroma social network";
+      after = [ "network-online.target" "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."/pleroma/config.exs".source ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "exec";
+        WorkingDirectory = "~";
+        StateDirectory = "pleroma pleroma/static pleroma/uploads";
+        StateDirectoryMode = "700";
+
+        # Checking the conf file is there then running the database
+        # migration before each service start, just in case there are
+        # some pending ones.
+        #
+        # It's sub-optimal as we'll always run this, even if pleroma
+        # has not been updated. But the no-op process is pretty fast.
+        # Better be safe than sorry migration-wise.
+        ExecStartPre =
+          let preScript = pkgs.writers.writeBashBin "pleromaStartPre"
+            "${cfg.package}/bin/pleroma_ctl migrate";
+          in "${preScript}/bin/pleromaStartPre";
+
+        ExecStart = "${cfg.package}/bin/pleroma start";
+        ExecStop = "${cfg.package}/bin/pleroma stop";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        # Systemd sandboxing directives.
+        # Taken from the upstream contrib systemd service at
+        # pleroma/installation/pleroma.service
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        PrivateDevices = false;
+        NoNewPrivileges = true;
+        CapabilityBoundingSet = "~CAP_SYS_ADMIN";
+      };
+    };
+
+  };
+  meta.maintainers = with lib.maintainers; [ ninjatrappeur ];
+  meta.doc = ./pleroma.xml;
+}
diff --git a/nixos/modules/services/networking/pleroma.xml b/nixos/modules/services/networking/pleroma.xml
new file mode 100644
index 0000000000000..9ab0be3d947c8
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.xml
@@ -0,0 +1,132 @@
+<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-pleroma">
+ <title>Pleroma</title>
+ <para><link xlink:href="https://pleroma.social/">Pleroma</link> is a lightweight activity pub server.</para>
+ <section xml:id="module-services-pleroma-getting-started">
+   <title>Quick Start</title>
+   <para>To get quickly started, you can use this sample NixOS configuration and adapt it to your use case.</para>
+   <para><programlisting>
+    {
+      security.acme = {
+        email = "root@tld";
+        acceptTerms = true;
+        certs = {
+          "social.tld.com" = {
+            webroot = "/var/www/social.tld.com";
+            email = "root@tld";
+            group = "nginx";
+          };
+        };
+      };
+      services = {
+        pleroma = {
+          enable = true;
+          secretConfigFile = "/var/lib/pleroma/secrets.exs";
+          configs = [
+          ''
+            import Config
+
+            config :pleroma, Pleroma.Web.Endpoint,
+            url: [host: "social.tld.com", scheme: "https", port: 443],
+            http: [ip: {127, 0, 0, 1}, port: 4000]
+
+            config :pleroma, :instance,
+            name: "NixOS test pleroma server",
+            email: "pleroma@social.tld.com",
+            notify_email: "pleroma@social.tld.com",
+            limit: 5000,
+            registrations_open: true
+
+            config :pleroma, :media_proxy,
+            enabled: false,
+            redirect_on_failure: true
+            #base_url: "https://cache.pleroma.social"
+
+            config :pleroma, Pleroma.Repo,
+            adapter: Ecto.Adapters.Postgres,
+            username: "pleroma",
+            password: "${test-db-passwd}",
+            database: "pleroma",
+            hostname: "localhost",
+            pool_size: 10,
+            prepare: :named,
+            parameters: [
+                plan_cache_mode: "force_custom_plan"
+            ]
+
+            config :pleroma, :database, rum_enabled: false
+            config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
+            config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
+            config :pleroma, configurable_from_database: false
+          ''
+          ];
+        };
+        postgresql = {
+          enable = true;
+          package = pkgs.postgresql_12;
+        };
+        nginx = {
+          enable = true;
+          addSSL = true;
+          sslCertificate = "/var/lib/acme/social.tld.com/fullchain.pem";
+          sslCertificateKey = "/var/lib/acme/social.tld.com/key.pem";
+          root = "/var/www/social.tld.com";
+          # ACME endpoint
+          locations."/.well-known/acme-challenge" = {
+              root = "/var/www/social.tld.com/";
+          };
+          virtualHosts."social.tld.com" = {
+            addSSL = true;
+            locations."/" = {
+              proxyPass = "http://127.0.0.1:4000";
+              extraConfig = ''
+                add_header 'Access-Control-Allow-Origin' '*' always;
+                add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+                add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+                add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+                if ($request_method = OPTIONS) {
+                    return 204;
+                }
+                add_header X-XSS-Protection "1; mode=block";
+                add_header X-Permitted-Cross-Domain-Policies none;
+                add_header X-Frame-Options DENY;
+                add_header X-Content-Type-Options nosniff;
+                add_header Referrer-Policy same-origin;
+                add_header X-Download-Options noopen;
+                proxy_http_version 1.1;
+                proxy_set_header Upgrade $http_upgrade;
+                proxy_set_header Connection "upgrade";
+                proxy_set_header Host $host;
+                client_max_body_size 16m;
+              '';
+            };
+          };
+        };
+      };
+    };
+   </programlisting></para>
+   <para>Note that you'll need to seed your database and upload your pleroma secrets to the path pointed by <literal>config.pleroma.secretConfigFile</literal>. You can find more informations about how to do that in the <link linkend="module-services-pleroma-generate-config">next</link> section.</para>
+ </section>
+ <section xml:id="module-services-pleroma-generate-config">
+   <title>Generating the Pleroma Config and Seed the Database</title>
+
+   <para>Before using this service, you'll need to generate your
+server configuration and its associated database seed. The
+<literal>pleroma_ctl</literal> CLI utility can help you with that. You
+can start with <literal>pleroma_ctl instance gen --output config.exs
+--output-psql setup.psql</literal>, this will prompt you some
+questions and will generate both your config file and database initial
+migration. </para>
+<para>For more details about this configuration format, please have a look at the <link xlink:href="https://docs-develop.pleroma.social/backend/configuration/cheatsheet/">upstream documentation</link>.</para>
+<para>To seed your database, you can use the <literal>setup.psql</literal> file you just generated by running
+<programlisting>
+    sudo -u postgres psql -f setup.psql
+</programlisting></para>
+   <para>In regard of the pleroma service configuration you also just generated, you'll need to split it in two parts. The "public" part, which do not contain any secrets and thus can be safely stored in the Nix store and its "private" counterpart containing some secrets (database password, endpoint secret key, salts, etc.).</para>
+
+   <para>The public part will live in your NixOS machine configuration in the <link linkend="opt-services.pleroma.configs">services.pleroma.configs</link> option. However, it's up to you to upload the secret pleroma configuration to the path pointed by <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>. You can do that manually or rely on a third party tool such as <link xlink:href="https://github.com/DBCDK/morph">Morph</link> or <link xlink:href="https://github.com/NixOS/nixops">NixOps</link>.</para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
index ba05e15389f66..8cae61b835431 100644
--- a/nixos/modules/services/networking/powerdns.nix
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -8,42 +8,40 @@ let
 in {
   options = {
     services.powerdns = {
-      enable = mkEnableOption "Powerdns domain name server";
+      enable = mkEnableOption "PowerDNS domain name server";
 
       extraConfig = mkOption {
         type = types.lines;
         default = "launch=bind";
         description = ''
-          Extra lines to be added verbatim to pdns.conf.
-          Powerdns will chroot to /var/lib/powerdns.
-          So any file, powerdns is supposed to be read,
-          should be in /var/lib/powerdns and needs to specified
-          relative to the chroot.
+          PowerDNS configuration. Refer to
+          <link xlink:href="https://doc.powerdns.com/authoritative/settings.html"/>
+          for details on supported values.
         '';
       };
     };
   };
 
-  config = mkIf config.services.powerdns.enable {
+  config = mkIf cfg.enable {
+
+    systemd.packages = [ pkgs.powerdns ];
+
     systemd.services.pdns = {
-      unitConfig.Documentation = "man:pdns_server(1) man:pdns_control(1)";
-      description = "Powerdns name server";
       wantedBy = [ "multi-user.target" ];
-      after = ["network.target" "mysql.service" "postgresql.service" "openldap.service"];
+      after = [ "network.target" "mysql.service" "postgresql.service" "openldap.service" ];
 
       serviceConfig = {
-        Restart="on-failure";
-        RestartSec="1";
-        StartLimitInterval="0";
-        PrivateDevices=true;
-        CapabilityBoundingSet="CAP_CHOWN CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_SYS_CHROOT";
-        NoNewPrivileges=true;
-        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/lib/powerdns";
-        ExecStart = "${pkgs.powerdns}/bin/pdns_server --setuid=nobody --setgid=nogroup --chroot=/var/lib/powerdns --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${configDir}";
-        ProtectSystem="full";
-        ProtectHome=true;
-        RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
+        ExecStart = [ "" "${pkgs.powerdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
       };
     };
+
+    users.users.pdns = {
+      isSystemUser = true;
+      group = "pdns";
+      description = "PowerDNS";
+    };
+
+    users.groups.pdns = {};
+
   };
 }
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
index f04dac01d9b8e..ae9258b27122f 100644
--- a/nixos/modules/services/networking/prayer.nix
+++ b/nixos/modules/services/networking/prayer.nix
@@ -44,7 +44,8 @@ in
       enable = mkEnableOption "the prayer webmail http server";
 
       port = mkOption {
-        default = "2080";
+        default = 2080;
+        type = types.port;
         description = ''
           Port the prayer http server is listening to.
         '';
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index 1f41c720adf58..7caae3282032c 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -8,15 +8,22 @@ let
 
   cfg = config.services.privoxy;
 
-  confFile = pkgs.writeText "privoxy.conf" ''
+  confFile = pkgs.writeText "privoxy.conf" (''
     user-manual ${privoxy}/share/doc/privoxy/user-manual
     confdir ${privoxy}/etc/
     listen-address  ${cfg.listenAddress}
     enable-edit-actions ${if (cfg.enableEditActions == true) then "1" else "0"}
     ${concatMapStrings (f: "actionsfile ${f}\n") cfg.actionsFiles}
     ${concatMapStrings (f: "filterfile ${f}\n") cfg.filterFiles}
+  '' + optionalString cfg.enableTor ''
+    forward-socks5t / 127.0.0.1:9063 .
+    toggle 1
+    enable-remote-toggle 0
+    enable-edit-actions 0
+    enable-remote-http-toggle 0
+  '' + ''
     ${cfg.extraConfig}
-  '';
+  '');
 
 in
 
@@ -72,6 +79,15 @@ in
         '';
       };
 
+      enableTor = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to configure Privoxy to use Tor's faster SOCKS port,
+          suitable for HTTP.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "" ;
@@ -107,6 +123,11 @@ in
       serviceConfig.ProtectSystem = "full";
     };
 
+    services.tor.settings.SOCKSPort = mkIf cfg.enableTor [
+      # Route HTTP traffic over a faster port (without IsolateDestAddr).
+      { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; }
+    ];
+
   };
 
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index a6c1cb0f4797a..e7a7aa700be6e 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -261,7 +261,7 @@ let
 
   toLua = x:
     if builtins.isString x then ''"${x}"''
-    else if builtins.isBool x then (if x == true then "true" else "false")
+    else if builtins.isBool x then boolToString x
     else if builtins.isInt x then toString x
     else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
     else throw "Invalid Lua value";
diff --git a/nixos/modules/services/networking/quassel.nix b/nixos/modules/services/networking/quassel.nix
index da723ec86adf6..bfbd3b46ab4d9 100644
--- a/nixos/modules/services/networking/quassel.nix
+++ b/nixos/modules/services/networking/quassel.nix
@@ -45,6 +45,7 @@ in
       };
 
       interfaces = mkOption {
+        type = types.listOf types.str;
         default = [ "127.0.0.1" ];
         description = ''
           The interfaces the Quassel daemon will be listening to.  If `[ 127.0.0.1 ]',
@@ -54,6 +55,7 @@ in
       };
 
       portNumber = mkOption {
+        type = types.port;
         default = 4242;
         description = ''
           The port number the Quassel daemon will be listening to.
@@ -61,7 +63,8 @@ in
       };
 
       dataDir = mkOption {
-        default = ''/home/${user}/.config/quassel-irc.org'';
+        default = "/home/${user}/.config/quassel-irc.org";
+        type = types.str;
         description = ''
           The directory holding configuration files, the SQlite database and the SSL Cert.
         '';
@@ -69,6 +72,7 @@ in
 
       user = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           The existing user the Quassel daemon should run as. If left empty, a default "quassel" user will be created.
         '';
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
index f4b00c9b356ef..53fac4b7b72dc 100644
--- a/nixos/modules/services/networking/radvd.nix
+++ b/nixos/modules/services/networking/radvd.nix
@@ -33,6 +33,7 @@ in
     };
 
     services.radvd.config = mkOption {
+      type = types.lines;
       example =
         ''
           interface eth0 {
diff --git a/nixos/modules/services/networking/resilio.nix b/nixos/modules/services/networking/resilio.nix
index 6193d7340fc4a..4701b0e8143d2 100644
--- a/nixos/modules/services/networking/resilio.nix
+++ b/nixos/modules/services/networking/resilio.nix
@@ -183,6 +183,7 @@ in
 
       sharedFolders = mkOption {
         default = [];
+        type = types.listOf (types.attrsOf types.anything);
         example =
           [ { secret         = "AHMYFPCQAHBM7LQPFXQ7WV6Y42IGUXJ5Y";
               directory      = "/home/user/sync_test";
diff --git a/nixos/modules/services/networking/sabnzbd.nix b/nixos/modules/services/networking/sabnzbd.nix
index ff5aef7d1cb47..43566dfd25c5f 100644
--- a/nixos/modules/services/networking/sabnzbd.nix
+++ b/nixos/modules/services/networking/sabnzbd.nix
@@ -18,16 +18,19 @@ in
       enable = mkEnableOption "the sabnzbd server";
 
       configFile = mkOption {
+        type = types.path;
         default = "/var/lib/sabnzbd/sabnzbd.ini";
         description = "Path to config file.";
       };
 
       user = mkOption {
         default = "sabnzbd";
+        type = types.str;
         description = "User to run the service as";
       };
 
       group = mkOption {
+        type = types.str;
         default = "sabnzbd";
         description = "Group to run the service as";
       };
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index 60fb3d5d6d443..a515e4a3dc3ba 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -1,34 +1,114 @@
-{ config, lib, pkgs, ... }:
+{ options, config, lib, pkgs, ... }:
 
 with lib;
 
 let
-
+  runDir = "/run/searx";
   cfg = config.services.searx;
 
-  configFile = cfg.configFile;
+  generateConfig = ''
+    cd ${runDir}
+
+    # write NixOS settings as JSON
+    cat <<'EOF' > settings.yml
+      ${builtins.toJSON cfg.settings}
+    EOF
+
+    # substitute environment variables
+    env -0 | while IFS='=' read -r -d ''' n v; do
+      sed "s#@$n@#$v#g" -i settings.yml
+    done
+
+    # set strict permissions
+    chmod 400 settings.yml
+  '';
+
+  settingType = with types; (oneOf
+    [ bool int float str
+      (listOf settingType)
+      (attrsOf settingType)
+    ]) // { description = "JSON value"; };
 
 in
 
 {
 
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "searx" "configFile" ]
+      [ "services" "searx" "settingsFile" ])
+  ];
+
   ###### interface
 
   options = {
 
     services.searx = {
 
-      enable = mkEnableOption
-        "the searx server. See https://github.com/asciimoo/searx";
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        relatedPackages = [ "searx" ];
+        description = "Whether to enable Searx, the meta search engine.";
+      };
 
-      configFile = mkOption {
+      environmentFile = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "
-          The path of the Searx server configuration file. If no file
-          is specified, a default file is used (default config file has
-          debug mode enabled).
-        ";
+        description = ''
+          Environment file (see <literal>systemd.exec(5)</literal>
+          "EnvironmentFile=" section for the syntax) to define variables for
+          Searx. This option can be used to safely include secret keys into the
+          Searx configuration.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingType;
+        default = { };
+        example = literalExample ''
+          { server.port = 8080;
+            server.bind_address = "0.0.0.0";
+            server.secret_key = "@SEARX_SECRET_KEY@";
+
+            engines = lib.singleton
+              { name = "wolframalpha";
+                shortcut = "wa";
+                api_key = "@WOLFRAM_API_KEY@";
+                engine = "wolframalpha_api";
+              };
+          }
+        '';
+        description = ''
+          Searx settings. These will be merged with (taking precedence over)
+          the default configuration. It's also possible to refer to
+          environment variables
+          (defined in <xref linkend="opt-services.searx.environmentFile"/>)
+          using the syntax <literal>@VARIABLE_NAME@</literal>.
+          <note>
+            <para>
+              For available settings, see the Searx
+              <link xlink:href="https://searx.github.io/searx/admin/settings.html">docs</link>.
+            </para>
+          </note>
+        '';
+      };
+
+      settingsFile = mkOption {
+        type = types.path;
+        default = "${runDir}/settings.yml";
+        description = ''
+          The path of the Searx server settings.yml file. If no file is
+          specified, a default file is used (default config file has debug mode
+          enabled). Note: setting this options overrides
+          <xref linkend="opt-services.searx.settings"/>.
+          <warning>
+            <para>
+              This file, along with any secret key it contains, will be copied
+              into the world-readable Nix store.
+            </para>
+          </warning>
+        '';
       };
 
       package = mkOption {
@@ -38,6 +118,38 @@ in
         description = "searx package to use.";
       };
 
+      runInUwsgi = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run searx in uWSGI as a "vassal", instead of using its
+          built-in HTTP server. This is the recommended mode for public or
+          large instances, but is unecessary for LAN or local-only use.
+          <warning>
+            <para>
+              The built-in HTTP server logs all queries by default.
+            </para>
+          </warning>
+        '';
+      };
+
+      uwsgiConfig = mkOption {
+        type = options.services.uwsgi.instance.type;
+        default = { http = ":8080"; };
+        example = literalExample ''
+          {
+            disable-logging = true;
+            http = ":8080";                   # serve via HTTP...
+            socket = "/run/searx/searx.sock"; # ...or UNIX socket
+          }
+        '';
+        description = ''
+          Additional configuration of the uWSGI vassal running searx. It
+          should notably specify on which interfaces and ports the vassal
+          should listen.
+        '';
+      };
+
     };
 
   };
@@ -45,36 +157,74 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.searx.enable {
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
 
     users.users.searx =
-      { uid = config.ids.uids.searx;
-        description = "Searx user";
-        createHome = true;
-        home = "/var/lib/searx";
+      { description = "Searx daemon user";
+        group = "searx";
+        isSystemUser = true;
       };
 
-    users.groups.searx =
-      { gid = config.ids.gids.searx;
+    users.groups.searx = { };
+
+    systemd.services.searx-init = {
+      description = "Initialise Searx settings";
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = "searx";
+        RuntimeDirectory = "searx";
+        RuntimeDirectoryMode = "750";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      script = generateConfig;
+    };
+
+    systemd.services.searx = mkIf (!cfg.runInUwsgi) {
+      description = "Searx server, the meta search engine.";
+      wantedBy = [ "network.target" "multi-user.target" ];
+      requires = [ "searx-init.service" ];
+      after = [ "searx-init.service" ];
+      serviceConfig = {
+        User  = "searx";
+        Group = "searx";
+        ExecStart = "${cfg.package}/bin/searx-run";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      environment.SEARX_SETTINGS_PATH = cfg.settingsFile;
+    };
+
+    systemd.services.uwsgi = mkIf (cfg.runInUwsgi)
+      { requires = [ "searx-init.service" ];
+        after = [ "searx-init.service" ];
       };
 
-    systemd.services.searx =
-      {
-        description = "Searx server, the meta search engine.";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "searx";
-          ExecStart = "${cfg.package}/bin/searx-run";
-        };
-      } // (optionalAttrs (configFile != null) {
-        environment.SEARX_SETTINGS_PATH = configFile;
-      });
+    services.searx.settings = {
+      # merge NixOS settings with defaults settings.yml
+      use_default_settings = mkDefault true;
+    };
 
-    environment.systemPackages = [ cfg.package ];
+    services.uwsgi = mkIf (cfg.runInUwsgi) {
+      enable = true;
+      plugins = [ "python3" ];
+
+      instance.type = "emperor";
+      instance.vassals.searx = {
+        type = "normal";
+        strict = true;
+        immediate-uid = "searx";
+        immediate-gid = "searx";
+        lazy-apps = true;
+        enable-threads = true;
+        module = "searx.webapp";
+        env = [ "SEARX_SETTINGS_PATH=${cfg.settingsFile}" ];
+        pythonPackages = self: [ cfg.package ];
+      } // cfg.uwsgiConfig;
+    };
 
   };
 
-  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+  meta.maintainers = with maintainers; [ rnhmjoj ];
 
 }
diff --git a/nixos/modules/services/networking/shairport-sync.nix b/nixos/modules/services/networking/shairport-sync.nix
index b4b86a2d55bee..ac526c0e9f6f4 100644
--- a/nixos/modules/services/networking/shairport-sync.nix
+++ b/nixos/modules/services/networking/shairport-sync.nix
@@ -28,6 +28,7 @@ in
       };
 
       arguments = mkOption {
+        type = types.str;
         default = "-v -o pa";
         description = ''
           Arguments to pass to the daemon. Defaults to a local pulseaudio
@@ -36,6 +37,7 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "shairport";
         description = ''
           User account name under which to run shairport-sync. The account
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
new file mode 100644
index 0000000000000..4ce4b8250bc3c
--- /dev/null
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.shellhub-agent;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.shellhub-agent = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the ShellHub Agent daemon, which allows
+          secure remote logins.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.shellhub-agent;
+        defaultText = "pkgs.shellhub-agent";
+        description = ''
+          Which ShellHub Agent package to use.
+        '';
+      };
+
+      tenantId = mkOption {
+        type = types.str;
+        example = "ba0a880c-2ada-11eb-a35e-17266ef329d6";
+        description = ''
+          The tenant ID to use when connecting to the ShellHub
+          Gateway.
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        default = "https://cloud.shellhub.io";
+        description = ''
+          Server address of ShellHub Gateway to connect.
+        '';
+      };
+
+      privateKey = mkOption {
+        type = types.path;
+        default = "/var/lib/shellhub-agent/private.key";
+        description = ''
+          Location where to store the ShellHub Agent private
+          key.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.shellhub-agent = {
+      description = "ShellHub Agent";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "local-fs.target" ];
+      wants = [ "network-online.target" ];
+      after = [
+        "local-fs.target"
+        "network.target"
+        "network-online.target"
+        "time-sync.target"
+      ];
+
+      environment.SERVER_ADDRESS = cfg.server;
+      environment.PRIVATE_KEY = cfg.privateKey;
+      environment.TENANT_ID = cfg.tenantId;
+
+      serviceConfig = {
+        # The service starts sessions for different users.
+        User = "root";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/agent";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index 37ee2a8038908..4470c18fd5330 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -124,7 +124,8 @@ in
       };
       hostName = mkOption {
         type = types.str;
-        default = config.networking.hostName;
+        default = config.networking.fqdn;
+        defaultText = "\${config.networking.fqdn}";
         example = "somewhere.example.com";
         description = "DNS name for the urls generated in the cgi.";
       };
@@ -156,6 +157,7 @@ in
       ownerEmail = mkOption {
         type = types.str;
         default = "no-reply@${cfg.hostName}";
+        defaultText = "no-reply@\${hostName}";
         example = "no-reply@yourdomain.com";
         description = "Email contact for owner";
       };
@@ -239,18 +241,18 @@ in
       targetConfig = mkOption {
         type = types.lines;
         default = ''
-					probe = FPing
-					menu = Top
-					title = Network Latency Grapher
-					remark = Welcome to the SmokePing website of xxx Company. \
-									 Here you will learn all about the latency of our network.
-					+ Local
-					menu = Local
-					title = Local Network
-					++ LocalMachine
-					menu = Local Machine
-					title = This host
-					host = localhost
+          probe = FPing
+          menu = Top
+          title = Network Latency Grapher
+          remark = Welcome to the SmokePing website of xxx Company. \
+                   Here you will learn all about the latency of our network.
+          + Local
+          menu = Local
+          title = Local Network
+          ++ LocalMachine
+          menu = Local Machine
+          title = This host
+          host = localhost
         '';
         description = "Target configuration";
       };
@@ -303,7 +305,7 @@ in
         ${cfg.package}/bin/smokeping --check --config=${configPath}
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
-      script = ''${cfg.package}/bin/smokeping --config=${configPath} --nodaemon'';
+      script = "${cfg.package}/bin/smokeping --config=${configPath} --nodaemon";
     };
     systemd.services.thttpd = mkIf cfg.webService {
       wantedBy = [ "multi-user.target"];
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 41d0584080e48..862ff7df05407 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -29,6 +29,7 @@ in
 
       portNumber = mkOption {
         default = 22;
+        type = types.port;
         description = ''
           The port on which to listen for connections.
         '';
@@ -36,6 +37,7 @@ in
 
       interfaces = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of network interfaces where listening for connections.
           When providing the empty list, `[]', lshd listens on all
@@ -46,6 +48,7 @@ in
 
       hostKey = mkOption {
         default = "/etc/lsh/host-key";
+        type = types.str;
         description = ''
           Path to the server's private key.  Note that this key must
           have been created, e.g., using "lsh-keygen --server |
@@ -56,29 +59,30 @@ in
       syslog = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable syslog output.'';
+        description = "Whether to enable syslog output.";
       };
 
       passwordAuthentication = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable password authentication.'';
+        description = "Whether to enable password authentication.";
       };
 
       publicKeyAuthentication = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable public key authentication.'';
+        description = "Whether to enable public key authentication.";
       };
 
       rootLogin = mkOption {
         type = types.bool;
         default = false;
-        description = ''Whether to enable remote root login.'';
+        description = "Whether to enable remote root login.";
       };
 
       loginShell = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           If non-null, override the default login shell with the
           specified value.
@@ -88,6 +92,7 @@ in
 
       srpKeyExchange = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Whether to enable SRP key exchange and user authentication.
         '';
@@ -96,16 +101,17 @@ in
       tcpForwarding = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable TCP/IP forwarding.'';
+        description = "Whether to enable TCP/IP forwarding.";
       };
 
       x11Forwarding = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable X11 forwarding.'';
+        description = "Whether to enable X11 forwarding.";
       };
 
       subsystems = mkOption {
+        type = types.listOf types.path;
         description = ''
           List of subsystem-path pairs, where the head of the pair
           denotes the subsystem name, and the tail denotes the path to
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 5365b8b9b1072..3cc77e4cb9387 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -269,6 +269,7 @@ in
       kexAlgorithms = mkOption {
         type = types.listOf types.str;
         default = [
+          "curve25519-sha256"
           "curve25519-sha256@libssh.org"
           "diffie-hellman-group-exchange-sha256"
         ];
@@ -279,7 +280,7 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
@@ -300,7 +301,7 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
@@ -321,7 +322,7 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
@@ -476,7 +477,7 @@ in
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
+      [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.extraConfig = mkOrder 0
       ''
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index 0921febba668a..4c2740d201927 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -31,7 +31,7 @@ let
       { name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
       { name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
       { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
-      { name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; },
+      { name: "tls"; host: "localhost"; port: "443"; probe: "builtin"; },
       { name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
     );
   '';
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
index 0fec3ef00ad99..f67eedac29612 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/module.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -63,7 +63,7 @@ in  {
       description = "strongSwan IPsec IKEv1/IKEv2 daemon using swanctl";
       wantedBy = [ "multi-user.target" ];
       after    = [ "network-online.target" ];
-      path     = with pkgs; [ kmod iproute iptables utillinux ];
+      path     = with pkgs; [ kmod iproute iptables util-linux ];
       environment = {
         STRONGSWAN_CONF = pkgs.writeTextFile {
           name = "strongswan.conf";
diff --git a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
index 808cb863a9cf8..8ae62931a8f90 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
@@ -1173,20 +1173,20 @@ in {
 
     ppk = mkPrefixedAttrsOfParams {
       secret = mkOptionalStrParam ''
-	      Value of the PPK. It may either be an ASCII string, a hex encoded string
-	      if it has a <literal>0x</literal> prefix or a Base64 encoded string if
-	      it has a <literal>0s</literal> prefix in its value. Should have at least
-	      256 bits of entropy for 128-bit security.
+        Value of the PPK. It may either be an ASCII string, a hex encoded string
+        if it has a <literal>0x</literal> prefix or a Base64 encoded string if
+        it has a <literal>0s</literal> prefix in its value. Should have at least
+        256 bits of entropy for 128-bit security.
       '';
 
       id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
-	      PPK identity the PPK belongs to. Multiple unique identities may be
-	      specified, each having an <literal>id</literal> prefix, if a secret is
-	      shared between multiple peers.
+        PPK identity the PPK belongs to. Multiple unique identities may be
+        specified, each having an <literal>id</literal> prefix, if a secret is
+        shared between multiple peers.
       '';
     } ''
-	    Postquantum Preshared Key (PPK) section for a specific secret. Each PPK is
-	    defined in a unique section having the <literal>ppk</literal> prefix.
+      Postquantum Preshared Key (PPK) section for a specific secret. Each PPK is
+      defined in a unique section having the <literal>ppk</literal> prefix.
     '';
 
     private = mkPrefixedAttrsOfParams {
@@ -1273,7 +1273,7 @@ in {
         provided the user is prompted during an interactive
         <literal>--load-creds</literal> call.
       '';
-    } ''Definition for a private key that's stored on a token/smartcard/TPM.'';
+    } "Definition for a private key that's stored on a token/smartcard/TPM.";
 
   };
 
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 13a1a897c5ed8..f6170b8136545 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -152,7 +152,7 @@ in
     systemd.services.strongswan = {
       description = "strongSwan IPSec Service";
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ kmod iproute iptables utillinux ]; # XXX Linux
+      path = with pkgs; [ kmod iproute iptables util-linux ]; # XXX Linux
       after = [ "network-online.target" ];
       environment = {
         STRONGSWAN_CONF = strongswanConf { inherit setup connections ca secretsFile managePlugins enabledPlugins; };
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
index ab51bba2f6acf..fe1616f411f0a 100644
--- a/nixos/modules/services/networking/stunnel.nix
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -16,8 +16,12 @@ let
   serverConfig = {
     options = {
       accept = mkOption {
-        type = types.int;
-        description = "On which port stunnel should listen for incoming TLS connections.";
+        type = types.either types.str types.int;
+        description = ''
+          On which [host:]port stunnel should listen for incoming TLS connections.
+          Note that unlike other softwares stunnel ipv6 address need no brackets,
+          so to listen on all IPv6 addresses on port 1234 one would use ':::1234'.
+        '';
       };
 
       connect = mkOption {
@@ -129,7 +133,6 @@ in
         type = with types; attrsOf (submodule serverConfig);
         example = {
           fancyWebserver = {
-            enable = true;
             accept = 443;
             connect = 8080;
             cert = "/path/to/pem/file";
diff --git a/nixos/modules/services/networking/supybot.nix b/nixos/modules/services/networking/supybot.nix
index dc9fb31ffd0bf..332c3ced06f07 100644
--- a/nixos/modules/services/networking/supybot.nix
+++ b/nixos/modules/services/networking/supybot.nix
@@ -64,13 +64,14 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = p: [];
         description = ''
           Extra Python packages available to supybot plugins. The
           value must be a function which receives the attrset defined
           in <varname>python3Packages</varname> as the sole argument.
         '';
-        example = literalExample ''p: [ p.lxml p.requests ]'';
+        example = literalExample "p: [ p.lxml p.requests ]";
       };
 
     };
@@ -103,6 +104,8 @@ in
         rm -f '${cfg.stateDir}/supybot.cfg.bak'
       '';
 
+      startLimitIntervalSec = 5 * 60;  # 5 min
+      startLimitBurst = 1;
       serviceConfig = {
         ExecStart = "${pyEnv}/bin/supybot ${cfg.stateDir}/supybot.cfg";
         PIDFile = "/run/supybot.pid";
@@ -110,8 +113,6 @@ in
         Group = "supybot";
         UMask = "0007";
         Restart = "on-abort";
-        StartLimitInterval = "5m";
-        StartLimitBurst = "1";
 
         NoNewPrivileges = true;
         PrivateDevices = true;
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 4d6aeb75ebd15..1a1474595beba 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -14,36 +14,21 @@ in {
       default = 41641;
       description = "The port to listen on for tunnel traffic (0=autoselect).";
     };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.tailscale;
+      defaultText = "pkgs.tailscale";
+      description = "The package to use for tailscale";
+    };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.tailscale = {
-      description = "Tailscale client daemon";
-
-      after = [ "network-pre.target" ];
-      wants = [ "network-pre.target" ];
+    environment.systemPackages = [ cfg.package ]; # for the CLI
+    systemd.packages = [ cfg.package ];
+    systemd.services.tailscaled = {
       wantedBy = [ "multi-user.target" ];
-
-      unitConfig = {
-        StartLimitIntervalSec = 0;
-        StartLimitBurst = 0;
-      };
-
-      serviceConfig = {
-        ExecStart =
-          "${pkgs.tailscale}/bin/tailscaled --port ${toString cfg.port}";
-
-        RuntimeDirectory = "tailscale";
-        RuntimeDirectoryMode = 755;
-
-        StateDirectory = "tailscale";
-        StateDirectoryMode = 750;
-
-        CacheDirectory = "tailscale";
-        CacheDirectoryMode = 750;
-
-        Restart = "on-failure";
-      };
+      serviceConfig.Environment = "PORT=${toString cfg.port}";
     };
   };
 }
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
index 725bd9bf9403b..b6afd83a9abd8 100644
--- a/nixos/modules/services/networking/tinc.nix
+++ b/nixos/modules/services/networking/tinc.nix
@@ -1,13 +1,156 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.tinc;
 
-in
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  toTincConf = generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } "=";
+  };
+
+  tincConfType = with types;
+    let
+      valueType = oneOf [ bool str int ];
+    in
+    attrsOf (either valueType (listOf valueType));
+
+  addressSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = "The external IP address or hostname where the host can be reached.";
+      };
+
+      port = mkOption {
+        type = types.nullOr types.port;
+        default = null;
+        description = ''
+          The port where the host can be reached.
+
+          If no port is specified, the default Port is used.
+        '';
+      };
+    };
+  };
+
+  subnetSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = ''
+          The subnet of this host.
+
+          Subnets can either be single MAC, IPv4 or IPv6 addresses, in which case
+          a subnet consisting of only that single address is assumed, or they can
+          be a IPv4 or IPv6 network address with a prefix length.
+
+          IPv4 subnets are notated like 192.168.1.0/24, IPv6 subnets are notated
+          like fec0:0:0:1::/64. MAC addresses are notated like 0:1a:2b:3c:4d:5e.
+
+          Note that subnets like 192.168.1.1/24 are invalid.
+        '';
+      };
+
+      prefixLength = mkOption {
+        type = with types; nullOr (addCheck int (n: n >= 0 && n <= 128));
+        default = null;
+        description = ''
+          The prefix length of the subnet.
+
+          If null, a subnet consisting of only that single address is assumed.
+
+          This conforms to standard CIDR notation as described in RFC1519.
+        '';
+      };
+
+      weight = mkOption {
+        type = types.ints.unsigned;
+        default = 10;
+        description = ''
+          Indicates the priority over identical Subnets owned by different nodes.
+
+          Lower values indicate higher priority. Packets will be sent to the
+          node with the highest priority, unless that node is not reachable, in
+          which case the node with the next highest priority will be tried, and
+          so on.
+        '';
+      };
+    };
+  };
+
+  hostSubmodule = { config, ... }: {
+    options = {
+      addresses = mkOption {
+        type = types.listOf (types.submodule addressSubmodule);
+        default = [ ];
+        description = ''
+          The external address where the host can be reached. This will set this
+          host's <option>settings.Address</option> option.
+
+          This variable is only required if you want to connect to this host.
+        '';
+      };
+
+      subnets = mkOption {
+        type = types.listOf (types.submodule subnetSubmodule);
+        default = [ ];
+        description = ''
+          The subnets which this tinc daemon will serve. This will set this
+          host's <option>settings.Subnet</option> option.
 
+          Tinc tries to look up which other daemon it should send a packet to by
+          searching the appropriate subnet. If the packet matches a subnet, it
+          will be sent to the daemon who has this subnet in his host
+          configuration file.
+        '';
+      };
+
+      rsaPublicKey = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Legacy RSA public key of the host in PEM format, including start and
+          end markers.
+
+          This will be appended as-is in the host's configuration file.
+
+          The ed25519 public key can be specified using the
+          <option>settings.Ed25519PublicKey</option> option instead.
+        '';
+      };
+
+      settings = mkOption {
+        default = { };
+        type = types.submodule { freeformType = tincConfType; };
+        description = ''
+          Configuration for this host.
+
+          See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Host-configuration-variables.html"/>
+          for supported values.
+        '';
+      };
+    };
+
+    config.settings = {
+      Address = mkDefault (map
+        (address: "${address.address} ${toString address.port}")
+        config.addresses);
+
+      Subnet = mkDefault (map
+        (subnet:
+          if subnet.prefixLength == null then "${subnet.address}#${toString subnet.weight}"
+          else "${subnet.address}/${toString subnet.prefixLength}#${toString subnet.weight}")
+        config.subnets);
+    };
+  };
+
+in
 {
 
   ###### interface
@@ -18,7 +161,7 @@ in
 
       networks = mkOption {
         default = { };
-        type = with types; attrsOf (submodule {
+        type = with types; attrsOf (submodule ({ config, ... }: {
           options = {
 
             extraConfig = mkOption {
@@ -26,6 +169,9 @@ in
               type = types.lines;
               description = ''
                 Extra lines to add to the tinc service configuration file.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.settings</option>
+                option is preferred.
               '';
             };
 
@@ -72,6 +218,40 @@ in
               description = ''
                 The name of the host in the network as well as the configuration for that host.
                 This name should only contain alphanumerics and underscores.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.hostSettings</option>
+                option is preferred.
+              '';
+            };
+
+            hostSettings = mkOption {
+              default = { };
+              example = literalExample ''
+                {
+                  host1 = {
+                    addresses = [
+                      { address = "192.168.1.42"; }
+                      { address = "192.168.1.42"; port = 1655; }
+                    ];
+                    subnets = [ { address = "10.0.0.42"; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Ed25519PublicKey = "...";
+                    };
+                  };
+                  host2 = {
+                    subnets = [ { address = "10.0.1.0"; prefixLength = 24; weight = 2; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Compression = 10;
+                    };
+                  };
+                }
+              '';
+              type = types.attrsOf (types.submodule hostSubmodule);
+              description = ''
+                The name of the host in the network as well as the configuration for that host.
+                This name should only contain alphanumerics and underscores.
               '';
             };
 
@@ -79,7 +259,7 @@ in
               default = "tun";
               type = types.enum [ "tun" "tap" ];
               description = ''
-                The type of virtual interface used for the network connection
+                The type of virtual interface used for the network connection.
               '';
             };
 
@@ -118,8 +298,44 @@ in
                 Note that tinc can't run scripts anymore (such as tinc-down or host-up), unless it is setup to be runnable inside chroot environment.
               '';
             };
+
+            settings = mkOption {
+              default = { };
+              type = types.submodule { freeformType = tincConfType; };
+              example = literalExample ''
+                {
+                  Interface = "custom.interface";
+                  DirectOnly = true;
+                  Mode = "switch";
+                }
+              '';
+              description = ''
+                Configuration of the Tinc daemon for this network.
+
+                See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Main-configuration-variables.html"/>
+                for supported values.
+              '';
+            };
+          };
+
+          config = {
+            hosts = mapAttrs
+              (hostname: host: ''
+                ${toTincConf host.settings}
+                ${host.rsaPublicKey}
+              '')
+              config.hostSettings;
+
+            settings = {
+              DeviceType = mkDefault config.interfaceType;
+              Name = mkDefault (if config.name == null then "$HOST" else config.name);
+              Ed25519PrivateKeyFile = mkIf (config.ed25519PrivateKeyFile != null) (mkDefault config.ed25519PrivateKeyFile);
+              PrivateKeyFile = mkIf (config.rsaPrivateKeyFile != null) (mkDefault config.rsaPrivateKeyFile);
+              ListenAddress = mkIf (config.listenAddress != null) (mkDefault config.listenAddress);
+              BindToAddress = mkIf (config.bindToAddress != null) (mkDefault config.bindToAddress);
+            };
           };
-        });
+        }));
 
         description = ''
           Defines the tinc networks which will be started.
@@ -144,13 +360,7 @@ in
           "tinc/${network}/tinc.conf" = {
             mode = "0444";
             text = ''
-              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}
+              ${toTincConf ({ Interface = "tinc.${network}"; } // data.settings)}
               ${data.extraConfig}
             '';
           };
@@ -168,6 +378,7 @@ in
           Type = "simple";
           Restart = "always";
           RestartSec = "3";
+          ExecReload = mkIf (versionAtLeast (getVersion data.package) "1.1pre") "${data.package}/bin/tinc -n ${network} reload";
           ExecStart = "${data.package}/bin/tincd -D -U tinc.${network} -n ${network} ${optionalString (data.chroot) "-R"} --pidfile /run/tinc.${network}.pid -d ${toString data.debugLevel}";
         };
         preStart = ''
@@ -221,4 +432,5 @@ in
 
   };
 
+  meta.maintainers = with maintainers; [ minijackson ];
 }
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index baed83591e1ed..622c3d8ea434f 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -1,9 +1,7 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.unbound;
 
   stateDir = "/var/lib/unbound";
@@ -17,12 +15,12 @@ let
   forward =
     optionalString (any isLocalAddress cfg.forwardAddresses) ''
       do-not-query-localhost: no
-    '' +
-    optionalString (cfg.forwardAddresses != []) ''
+    ''
+    + optionalString (cfg.forwardAddresses != []) ''
       forward-zone:
         name: .
-    '' +
-    concatMapStringsSep "\n" (x: "    forward-addr: ${x}") cfg.forwardAddresses;
+    ''
+    + concatMapStringsSep "\n" (x: "    forward-addr: ${x}") cfg.forwardAddresses;
 
   rootTrustAnchorFile = "${stateDir}/root.key";
 
@@ -31,19 +29,25 @@ let
 
   confFile = pkgs.writeText "unbound.conf" ''
     server:
+      ip-freebind: yes
       directory: "${stateDir}"
       username: unbound
-      chroot: "${stateDir}"
+      chroot: ""
       pidfile: ""
+      # when running under systemd there is no need to daemonize
+      do-daemonize: no
       ${interfaces}
       ${access}
       ${trustAnchor}
+    ${lib.optionalString (cfg.localControlSocketPath != null) ''
+      remote-control:
+        control-enable: yes
+        control-interface: ${cfg.localControlSocketPath}
+    ''}
     ${cfg.extraConfig}
     ${forward}
   '';
-
 in
-
 {
 
   ###### interface
@@ -55,8 +59,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.unbound;
-        defaultText = "pkgs.unbound";
+        default = pkgs.unbound-with-systemd;
+        defaultText = "pkgs.unbound-with-systemd";
         description = "The unbound package to use";
       };
 
@@ -69,11 +73,14 @@ in
       interfaces = mkOption {
         default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
         type = types.listOf types.str;
-        description = "What addresses the server should listen on.";
+        description =  ''
+          What addresses the server should listen on. This supports the interface syntax documented in
+          <citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+        '';
       };
 
       forwardAddresses = mkOption {
-        default = [ ];
+        default = [];
         type = types.listOf types.str;
         description = "What servers to forward queries to.";
       };
@@ -84,6 +91,28 @@ in
         description = "Use and update root trust anchor for DNSSEC validation.";
       };
 
+      localControlSocketPath = mkOption {
+        default = null;
+        # FIXME: What is the proper type here so users can specify strings,
+        # paths and null?
+        # My guess would be `types.nullOr (types.either types.str types.path)`
+        # but I haven't verified yet.
+        type = types.nullOr types.str;
+        example = "/run/unbound/unbound.ctl";
+        description = ''
+          When not set to <literal>null</literal> this option defines the path
+          at which the unbound remote control socket should be created at. The
+          socket will be owned by the unbound user (<literal>unbound</literal>)
+          and group will be <literal>nogroup</literal>.
+
+          Users that should be permitted to access the socket must be in the
+          <literal>unbound</literal> group.
+
+          If this option is <literal>null</literal> remote control will not be
+          configured at all. Unbounds default values apply.
+        '';
+      };
+
       extraConfig = mkOption {
         default = "";
         type = types.lines;
@@ -106,43 +135,85 @@ in
     users.users.unbound = {
       description = "unbound daemon user";
       isSystemUser = true;
+      group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
+    };
+
+    # We need a group so that we can give users access to the configured
+    # control socket. Unbound allows access to the socket only to the unbound
+    # user and the primary group.
+    users.groups = lib.mkIf (cfg.localControlSocketPath != null) {
+      unbound = {};
     };
 
     networking.resolvconf.useLocalResolver = mkDefault true;
 
+
+    environment.etc."unbound/unbound.conf".source = confFile;
+
     systemd.services.unbound = {
       description = "Unbound recursive Domain Name Server";
       after = [ "network.target" ];
       before = [ "nss-lookup.target" ];
-      wants = [ "nss-lookup.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}/dev/
-        cp ${confFile} ${stateDir}/unbound.conf
-        ${optionalString cfg.enableRootTrustAnchor ''
-          ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
-          chown unbound ${stateDir} ${rootTrustAnchorFile}
-        ''}
-        touch ${stateDir}/dev/random
-        ${pkgs.utillinux}/bin/mount --bind -n /dev/urandom ${stateDir}/dev/random
+      wantedBy = [ "multi-user.target" "nss-lookup.target" ];
+
+      preStart = lib.mkIf cfg.enableRootTrustAnchor ''
+        ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
       '';
 
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf";
-        ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random";
+      restartTriggers = [
+        confFile
+      ];
 
-        ProtectSystem = true;
-        ProtectHome = true;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
+        ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
+
+        NotifyAccess = "main";
+        Type = "notify";
+
+        # FIXME: Which of these do we actualy need, can we drop the chroot flag?
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+          "CAP_SETGID"
+          "CAP_SETUID"
+          "CAP_SYS_CHROOT"
+          "CAP_SYS_RESOURCE"
+        ];
+
+        User = "unbound";
+        Group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
+
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateDevices = true;
-        Restart = "always";
-        RestartSec = "5s";
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectSystem = "strict";
+        RuntimeDirectory = "unbound";
+        ConfigurationDirectory = "unbound";
+        StateDirectory = "unbound";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "~@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@keyring"
+          "@module"
+          "mount"
+          "@obsolete"
+          "@resources"
+        ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictSUIDSGID = true;
       };
     };
-
     # If networkmanager is enabled, ask it to interface with unbound.
     networking.networkmanager.dns = "unbound";
-
   };
-
 }
diff --git a/nixos/modules/services/networking/wakeonlan.nix b/nixos/modules/services/networking/wakeonlan.nix
index ebfba263cd8f8..35ff67937fc7d 100644
--- a/nixos/modules/services/networking/wakeonlan.nix
+++ b/nixos/modules/services/networking/wakeonlan.nix
@@ -51,6 +51,6 @@ in
 
   ###### implementation
 
-  config.powerManagement.powerDownCommands = lines;
+  config.powerManagement.powerUpCommands = lines;
 
 }
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
index 6eacffe709b0d..8482823e197f7 100644
--- a/nixos/modules/services/networking/wasabibackend.nix
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -21,7 +21,7 @@ let
       RegTestBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
   };
 
-	configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
+  configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
 
 in {
 
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 395139879036a..61482596763a6 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -14,8 +14,8 @@ let
         then ''"${psk}"''
         else pskRaw;
       baseAuth = if key != null
-        then ''psk=${key}''
-        else ''key_mgmt=NONE'';
+        then "psk=${key}"
+        else "key_mgmt=NONE";
     in ''
       network={
         ssid="${ssid}"
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index e67badfcd29eb..b19cd07325266 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -270,7 +270,7 @@ in
       drivers = mkOption {
         type = types.listOf types.path;
         default = [];
-        example = literalExample "with pkgs; [ gutenprint hplip splix cups-googlecloudprint ]";
+        example = literalExample "with pkgs; [ gutenprint hplip splix ]";
         description = ''
           CUPS drivers to use. Drivers provided by CUPS, cups-filters,
           Ghostscript and Samba are added unconditionally. If this list contains
diff --git a/nixos/modules/services/search/elasticsearch-curator.nix b/nixos/modules/services/search/elasticsearch-curator.nix
index 9620c3e0b6d49..bb2612322bbad 100644
--- a/nixos/modules/services/search/elasticsearch-curator.nix
+++ b/nixos/modules/services/search/elasticsearch-curator.nix
@@ -55,6 +55,7 @@ in {
     };
     actionYAML = mkOption {
       description = "curator action.yaml file contents, alternatively use curator-cli which takes a simple action command";
+      type = types.lines;
       example = ''
         ---
         actions:
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 3f84f9c2560cc..cf0d72d5c5319 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -282,12 +282,12 @@ in
     services.fail2ban.jails.DEFAULT = ''
       ${optionalString cfg.bantime-increment.enable ''
         # Bantime incremental
-        bantime.increment    = ${if cfg.bantime-increment.enable then "true" else "false"}
+        bantime.increment    = ${boolToString cfg.bantime-increment.enable}
         bantime.maxtime      = ${cfg.bantime-increment.maxtime}
         bantime.factor       = ${cfg.bantime-increment.factor}
         bantime.formula      = ${cfg.bantime-increment.formula}
         bantime.multipliers  = ${cfg.bantime-increment.multipliers}
-        bantime.overalljails = ${if cfg.bantime-increment.overalljails then "true" else "false"}
+        bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}
       ''}
       # Miscellaneous options
       ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
index cbac4ef05b8d3..48f8a9616c3ed 100644
--- a/nixos/modules/services/security/fprintd.nix
+++ b/nixos/modules/services/security/fprintd.nix
@@ -43,9 +43,9 @@ in
 
   config = mkIf cfg.enable {
 
-    services.dbus.packages = [ pkgs.fprintd ];
+    services.dbus.packages = [ cfg.package ];
 
-    environment.systemPackages = [ pkgs.fprintd ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.packages = [ cfg.package ];
 
diff --git a/nixos/modules/services/security/fprot.nix b/nixos/modules/services/security/fprot.nix
index 3a0b08b3c6d83..df60d553e85bd 100644
--- a/nixos/modules/services/security/fprot.nix
+++ b/nixos/modules/services/security/fprot.nix
@@ -16,16 +16,19 @@ in {
           description = ''
             product.data file. Defaults to the one supplied with installation package.
           '';
+          type = types.path;
         };
 
         frequency = mkOption {
           default = 30;
+          type = types.int;
           description = ''
             Update virus definitions every X minutes.
           '';
         };
 
         licenseKeyfile = mkOption {
+          type = types.path;
           description = ''
             License keyfile. Defaults to the one supplied with installation package.
           '';
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 2f9e94bd77bac..77c579279abe5 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -448,7 +448,7 @@ in
       default = false;
       description = ''
         In case when running behind a reverse proxy, controls whether headers
-	like <literal>X-Real-Ip</literal> are accepted. Usage behind a reverse
+        like <literal>X-Real-Ip</literal> are accepted. Usage behind a reverse
         proxy will require this flag to be set to avoid logging the reverse
         proxy IP address.
       '';
@@ -524,7 +524,7 @@ in
       type = types.nullOr types.str;
       default = null;
       description = ''
-      	Profile access endpoint.
+        Profile access endpoint.
       '';
     };
 
@@ -538,6 +538,7 @@ in
 
     extraConfig = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       description = ''
         Extra config to pass to oauth2-proxy.
       '';
diff --git a/nixos/modules/services/security/oauth2_proxy_nginx.nix b/nixos/modules/services/security/oauth2_proxy_nginx.nix
index be6734f439f3d..553638ad49658 100644
--- a/nixos/modules/services/security/oauth2_proxy_nginx.nix
+++ b/nixos/modules/services/security/oauth2_proxy_nginx.nix
@@ -31,7 +31,7 @@ in
         proxyPass = cfg.proxy;
         extraConfig = ''
           proxy_set_header X-Scheme                $scheme;
-          proxy_set_header X-Auth-Request-Redirect $request_uri;
+          proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
         '';
       };
       locations."/oauth2/auth" = {
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index e7a9cefdef30a..72de11a9254cd 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -119,15 +119,17 @@ in {
       # firewall rules before sshguard starts.
       preStart = optionalString config.networking.firewall.enable ''
         ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
-        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
         ${pkgs.iptables}/bin/iptables  -I INPUT -m set --match-set sshguard4 src -j DROP
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
         ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
       '';
 
       postStop = optionalString config.networking.firewall.enable ''
         ${pkgs.iptables}/bin/iptables  -D INPUT -m set --match-set sshguard4 src -j DROP
-        ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
         ${pkgs.ipset}/bin/ipset -quiet destroy sshguard4
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
         ${pkgs.ipset}/bin/ipset -quiet destroy sshguard6
       '';
 
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index 38dc378887a83..54c2c2dea23af 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -1,311 +1,300 @@
 { config, lib, pkgs, ... }:
 
+with builtins;
 with lib;
 
 let
   cfg = config.services.tor;
-  torDirectory = "/var/lib/tor";
-  torRunDirectory = "/run/tor";
-
-  opt    = name: value: optionalString (value != null) "${name} ${value}";
-  optint = name: value: optionalString (value != null && value != 0)    "${name} ${toString value}";
-
-  isolationOptions = {
-    type = types.listOf (types.enum [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+  stateDir = "/var/lib/tor";
+  runDir = "/run/tor";
+  descriptionGeneric = option: ''
+    See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en#${option}">torrc manual</link>.
+  '';
+  bindsPrivilegedPort =
+    any (p0:
+      let p1 = if p0 ? "port" then p0.port else p0; in
+      if p1 == "auto" then false
+      else let p2 = if isInt p1 then p1 else toInt p1; in
+        p1 != null && 0 < p2 && p2 < 1024)
+    (flatten [
+      cfg.settings.ORPort
+      cfg.settings.DirPort
+      cfg.settings.DNSPort
+      cfg.settings.ExtORPort
+      cfg.settings.HTTPTunnelPort
+      cfg.settings.NATDPort
+      cfg.settings.SOCKSPort
+      cfg.settings.TransPort
     ]);
+  optionBool = optionName: mkOption {
+    type = with types; nullOr bool;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionInt = optionName: mkOption {
+    type = with types; nullOr int;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionString = optionName: mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionStrings = optionName: mkOption {
+    type = with types; listOf str;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionAddress = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    example = "0.0.0.0";
+    description = ''
+      IPv4 or IPv6 (if between brackets) address.
+    '';
+  };
+  optionUnix = mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = ''
+      Unix domain socket path to use.
+    '';
+  };
+  optionPort = mkOption {
+    type = with types; nullOr (oneOf [port (enum ["auto"])]);
+    default = null;
+  };
+  optionPorts = optionName: mkOption {
+    type = with types; listOf port;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionIsolablePort = with types; oneOf [
+    port (enum ["auto"])
+    (submodule ({config, ...}: {
+      options = {
+        addr = optionAddress;
+        port = optionPort;
+        flags = optionFlags;
+        SessionGroup = mkOption { type = nullOr int; default = null; };
+      } // genAttrs isolateFlags (name: mkOption { type = types.bool; default = false; });
+      config = {
+        flags = filter (name: config.${name} == true) isolateFlags ++
+                optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+      };
+    }))
+  ];
+  optionIsolablePorts = optionName: mkOption {
     default = [];
-    example = [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+    type = with types; either optionIsolablePort (listOf optionIsolablePort);
+    description = descriptionGeneric optionName;
+  };
+  isolateFlags = [
+    "IsolateClientAddr"
+    "IsolateClientProtocol"
+    "IsolateDestAddr"
+    "IsolateDestPort"
+    "IsolateSOCKSAuth"
+    "KeepAliveIsolateSOCKSAuth"
+  ];
+  optionSOCKSPort = doConfig: let
+    flags = [
+      "CacheDNS" "CacheIPv4DNS" "CacheIPv6DNS" "GroupWritable" "IPv6Traffic"
+      "NoDNSRequest" "NoIPv4Traffic" "NoOnionTraffic" "OnionTrafficOnly"
+      "PreferIPv6" "PreferIPv6Automap" "PreferSOCKSNoAuth" "UseDNSCache"
+      "UseIPv4Cache" "UseIPv6Cache" "WorldWritable"
+    ] ++ isolateFlags;
+    in with types; oneOf [
+      port (submodule ({config, ...}: {
+        options = {
+          unix = optionUnix;
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+          SessionGroup = mkOption { type = nullOr int; default = null; };
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = mkIf doConfig { # Only add flags in SOCKSPort to avoid duplicates
+          flags = filter (name: config.${name} == true) flags ++
+                  optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+        };
+      }))
     ];
-    description = "Tor isolation options";
+  optionFlags = mkOption {
+    type = with types; listOf str;
+    default = [];
+  };
+  optionORPort = optionName: mkOption {
+    default = [];
+    example = 443;
+    type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+      port
+      (enum ["auto"])
+      (submodule ({config, ...}:
+        let flags = [ "IPv4Only" "IPv6Only" "NoAdvertise" "NoListen" ];
+        in {
+        options = {
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = {
+          flags = filter (name: config.${name} == true) flags;
+        };
+      }))
+    ]))];
+    description = descriptionGeneric optionName;
+  };
+  optionBandwith = optionName: mkOption {
+    type = with types; nullOr (either int str);
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionPath = optionName: mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = descriptionGeneric optionName;
   };
 
-
-  torRc = ''
-    User tor
-    DataDirectory ${torDirectory}
-    ${optionalString cfg.enableGeoIP ''
-      GeoIPFile ${cfg.package.geoip}/share/tor/geoip
-      GeoIPv6File ${cfg.package.geoip}/share/tor/geoip6
-    ''}
-
-    ${optint "ControlPort" cfg.controlPort}
-    ${optionalString cfg.controlSocket.enable "ControlPort unix:${torRunDirectory}/control GroupWritable RelaxDirModeCheck"}
-  ''
-  # Client connection config
-  + optionalString cfg.client.enable ''
-    SOCKSPort ${cfg.client.socksListenAddress} ${toString cfg.client.socksIsolationOptions}
-    SOCKSPort ${cfg.client.socksListenAddressFaster}
-    ${opt "SocksPolicy" cfg.client.socksPolicy}
-
-    ${optionalString cfg.client.transparentProxy.enable ''
-    TransPort ${cfg.client.transparentProxy.listenAddress} ${toString cfg.client.transparentProxy.isolationOptions}
-    ''}
-
-    ${optionalString cfg.client.dns.enable ''
-    DNSPort ${cfg.client.dns.listenAddress} ${toString cfg.client.dns.isolationOptions}
-    AutomapHostsOnResolve 1
-    AutomapHostsSuffixes ${concatStringsSep "," cfg.client.dns.automapHostsSuffixes}
-    ''}
-  ''
-  # Explicitly disable the SOCKS server if the client is disabled.  In
-  # particular, this makes non-anonymous hidden services possible.
-  + optionalString (! cfg.client.enable) ''
-  SOCKSPort 0
-  ''
-  # Relay config
-  + optionalString cfg.relay.enable ''
-    ORPort ${toString cfg.relay.port}
-    ${opt "Address" cfg.relay.address}
-    ${opt "Nickname" cfg.relay.nickname}
-    ${opt "ContactInfo" cfg.relay.contactInfo}
-
-    ${optint "RelayBandwidthRate" cfg.relay.bandwidthRate}
-    ${optint "RelayBandwidthBurst" cfg.relay.bandwidthBurst}
-    ${opt "AccountingMax" cfg.relay.accountingMax}
-    ${opt "AccountingStart" cfg.relay.accountingStart}
-
-    ${if (cfg.relay.role == "exit") then
-        opt "ExitPolicy" cfg.relay.exitPolicy
-      else
-        "ExitPolicy reject *:*"}
-
-    ${optionalString (elem cfg.relay.role ["bridge" "private-bridge"]) ''
-      BridgeRelay 1
-      ServerTransportPlugin ${concatStringsSep "," cfg.relay.bridgeTransports} exec ${pkgs.obfs4}/bin/obfs4proxy managed
-      ExtORPort auto
-      ${optionalString (cfg.relay.role == "private-bridge") ''
-        ExtraInfoStatistics 0
-        PublishServerDescriptor 0
-      ''}
-    ''}
-  ''
-  # Hidden services
-  + concatStrings (flip mapAttrsToList cfg.hiddenServices (n: v: ''
-    HiddenServiceDir ${torDirectory}/onion/${v.name}
-    ${optionalString (v.version != null) "HiddenServiceVersion ${toString v.version}"}
-    ${flip concatMapStrings v.map (p: ''
-      HiddenServicePort ${toString p.port} ${p.destination}
-    '')}
-    ${optionalString (v.authorizeClient != null) ''
-      HiddenServiceAuthorizeClient ${v.authorizeClient.authType} ${concatStringsSep "," v.authorizeClient.clientNames}
-    ''}
-  ''))
-  + cfg.extraConfig;
-
-  torRcFile = pkgs.writeText "torrc" torRc;
-
+  mkValueString = k: v:
+    if v == null then ""
+    else if isBool v then
+      (if v then "1" else "0")
+    else if v ? "unix" && v.unix != null then
+      "unix:"+v.unix +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if v ? "port" && v.port != null then
+      optionalString (v ? "addr" && v.addr != null) "${v.addr}:" +
+      toString v.port +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if k == "ServerTransportPlugin" then
+      optionalString (v.transports != []) "${concatStringsSep "," v.transports} exec ${v.exec}"
+    else if k == "HidServAuth" then
+      concatMapStringsSep "\n${k} " (settings: settings.onion + " " settings.auth) v
+    else generators.mkValueStringDefault {} v;
+  genTorrc = settings:
+    generators.toKeyValue {
+      listsAsDuplicateKeys = true;
+      mkKeyValue = k: generators.mkKeyValueDefault { mkValueString = mkValueString k; } " " k;
+    }
+    (lib.mapAttrs (k: v:
+      # Not necesssary, but prettier rendering
+      if elem k [ "AutomapHostsSuffixes" "DirPolicy" "ExitPolicy" "SocksPolicy" ]
+      && v != []
+      then concatStringsSep "," v
+      else v)
+    (lib.filterAttrs (k: v: !(v == null || v == ""))
+    settings));
+  torrc = pkgs.writeText "torrc" (
+    genTorrc cfg.settings +
+    concatStrings (mapAttrsToList (name: onion:
+      "HiddenServiceDir ${onion.path}\n" +
+      genTorrc onion.settings) cfg.relay.onionServices)
+  );
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ])
+    (mkRenamedOptionModule [ "services" "tor" "client" "dns" "automapHostsSuffixes" ] [ "services" "tor" "settings" "AutomapHostsSuffixes" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "isolationOptions" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "listenAddress" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "privoxy" "enable" ] "Use services.privoxy.enable and services.privoxy.enableTor instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksIsolationOptions" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksListenAddressFaster" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "client" "socksPolicy" ] [ "services" "tor" "settings" "SocksPolicy" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "isolationOptions" ] "Use services.tor.settings.TransPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "listenAddress" ] "Use services.tor.settings.TransPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "controlPort" ] [ "services" "tor" "settings" "ControlPort" ])
+    (mkRemovedOptionModule [ "services" "tor" "extraConfig" ] "Plese use services.tor.settings instead.")
+    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "onionServices" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingMax" ] [ "services" "tor" "settings" "AccountingMax" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingStart" ] [ "services" "tor" "settings" "AccountingStart" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "address" ] [ "services" "tor" "settings" "Address" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthBurst" ] [ "services" "tor" "settings" "BandwidthBurst" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthRate" ] [ "services" "tor" "settings" "BandwidthRate" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bridgeTransports" ] [ "services" "tor" "settings" "ServerTransportPlugin" "transports" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "contactInfo" ] [ "services" "tor" "settings" "ContactInfo" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "exitPolicy" ] [ "services" "tor" "settings" "ExitPolicy" ])
     (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.")
     (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.")
+    (mkRenamedOptionModule [ "services" "tor" "relay" "nickname" ] [ "services" "tor" "settings" "Nickname" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "port" ] [ "services" "tor" "settings" "ORPort" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "settings" "ORPort" ])
   ];
 
   options = {
     services.tor = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable the Tor daemon. By default, the daemon is run without
-          relay, exit, bridge or client connectivity.
-        '';
-      };
+      enable = mkEnableOption ''Tor daemon.
+        By default, the daemon is run without
+        relay, exit, bridge or client connectivity'';
+
+      openFirewall = mkEnableOption "opening of the relay port(s) in the firewall";
 
       package = mkOption {
         type = types.package;
         default = pkgs.tor;
         defaultText = "pkgs.tor";
         example = literalExample "pkgs.tor";
-        description = ''
-          Tor package to use
-        '';
+        description = "Tor package to use.";
       };
 
-      enableGeoIP = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whenever to configure Tor daemon to use GeoIP databases.
+      enableGeoIP = mkEnableOption ''use of GeoIP databases.
+        Disabling this will disable by-country statistics for bridges and relays
+        and some client and third-party software functionality'' // { default = true; };
 
-          Disabling this will disable by-country statistics for
-          bridges and relays and some client and third-party software
-          functionality.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Extra configuration. Contents will be added verbatim to the
-          configuration file at the end.
-        '';
-      };
-
-      controlPort = mkOption {
-        type = types.nullOr (types.either types.int types.str);
-        default = null;
-        example = 9051;
-        description = ''
-          If set, Tor will accept connections on the specified port
-          and allow them to control the tor process.
-        '';
-      };
-
-      controlSocket = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor control socket. Control socket is created
-            in <literal>${torRunDirectory}/control</literal>
-          '';
-        };
-      };
+      controlSocket.enable = mkEnableOption ''control socket,
+        created in <literal>${runDir}/control</literal>'';
 
       client = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor daemon to route application
-            connections.  You might want to disable this if you plan
-            running a dedicated Tor relay.
-          '';
-        };
+        enable = mkEnableOption ''the routing of application connections.
+          You might want to disable this if you plan running a dedicated Tor relay'';
+
+        transparentProxy.enable = mkEnableOption "transparent proxy";
+        dns.enable = mkEnableOption "DNS resolver";
 
         socksListenAddress = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9050";
-          example = "192.168.0.1:9100";
+          type = optionSOCKSPort false;
+          default = {addr = "127.0.0.1"; port = 9050; IsolateDestAddr = true;};
+          example = {addr = "192.168.0.1"; port = 9090; IsolateDestAddr = true;};
           description = ''
             Bind to this address to listen for connections from
-            Socks-speaking applications. Provides strong circuit
-            isolation, separate circuit per IP address.
+            Socks-speaking applications.
           '';
         };
 
-        socksListenAddressFaster = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9063";
-          example = "192.168.0.1:9101";
-          description = ''
-            Bind to this address to listen for connections from
-            Socks-speaking applications. Same as
-            <option>socksListenAddress</option> but uses weaker
-            circuit isolation to provide performance suitable for a
-            web browser.
-           '';
-         };
-
-        socksPolicy = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "accept 192.168.0.0/16, reject *";
-          description = ''
-            Entry policies to allow/deny SOCKS requests based on IP
-            address. First entry that matches wins. If no SocksPolicy
-            is set, we accept all (and only) requests from
-            <option>socksListenAddress</option>.
-          '';
-        };
-
-        socksIsolationOptions = mkOption (isolationOptions // {
-          default = ["IsolateDestAddr"];
-        });
-
-        transparentProxy = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor transparent proxy";
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" = {
+              clientAuthorizations = ["/run/keys/tor/alice.prv.x25519"];
+            };
           };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9040";
-            example = "192.168.0.1:9040";
-            description = ''
-              Bind transparent proxy to this address.
-            '';
-          };
-
-          isolationOptions = mkOption isolationOptions;
-        };
-
-        dns = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor dns resolver";
-          };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9053";
-            example = "192.168.0.1:9053";
-            description = ''
-              Bind tor dns to this address.
-            '';
-          };
-
-          isolationOptions = mkOption isolationOptions;
-
-          automapHostsSuffixes = mkOption {
-            type = types.listOf types.str;
-            default = [".onion" ".exit"];
-            example = [".onion"];
-            description = "List of suffixes to use with automapHostsOnResolve";
-          };
-        };
-
-        privoxy.enable = mkOption {
-          type = types.bool;
-          default = true;
-          description = ''
-            Whether to enable and configure the system Privoxy to use Tor's
-            faster port, suitable for HTTP.
-
-            To have anonymity, protocols need to be scrubbed of identifying
-            information, and this can be accomplished for HTTP by Privoxy.
-
-            Privoxy can also be useful for KDE torification. A good setup would be:
-            setting SOCKS proxy to the default Tor port, providing maximum
-            circuit isolation where possible; and setting HTTP proxy to Privoxy
-            to route HTTP traffic over faster, but less isolated port.
-          '';
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.clientAuthorizations = mkOption {
+              description = ''
+                Clients' authorizations for a v3 onion service,
+                as a list of files containing each one private key, in the format:
+                <screen>descriptor:x25519:&lt;base32-private-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf path;
+              default = [];
+              example = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          }));
         };
       };
 
       relay = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable relaying TOR traffic for others.
+        enable = mkEnableOption ''relaying of Tor traffic for others.
 
-            See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
-            for details.
+          See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
+          for details.
 
-            Setting this to true requires setting
-            <option>services.tor.relay.role</option>
-            and
-            <option>services.tor.relay.port</option>
-            options.
-          '';
-        };
+          Setting this to true requires setting
+          <option>services.tor.relay.role</option>
+          and
+          <option>services.tor.settings.ORPort</option>
+          options'';
 
         role = mkOption {
           type = types.enum [ "exit" "relay" "bridge" "private-bridge" ];
@@ -324,13 +313,13 @@ in
                 <important><para>
                   Running an exit relay may expose you to abuse
                   complaints. See
-                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies" />
+                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies"/>
                   for more info.
                 </para></important>
 
                 <para>
                   You can specify which services Tor users may access via
-                  your exit relay using <option>exitPolicy</option> option.
+                  your exit relay using <option>settings.ExitPolicy</option> option.
                 </para>
               </listitem>
             </varlistentry>
@@ -383,15 +372,14 @@ in
                 <important>
                   <para>
                     WARNING: THE FOLLOWING PARAGRAPH IS NOT LEGAL ADVICE.
-                    Consult with your lawer when in doubt.
+                    Consult with your lawyer when in doubt.
                   </para>
 
                   <para>
                     This role should be safe to use in most situations
                     (unless the act of forwarding traffic for others is
                     a punishable offence under your local laws, which
-                    would be pretty insane as it would make ISP
-                    illegal).
+                    would be pretty insane as it would make ISP illegal).
                   </para>
                 </important>
 
@@ -418,7 +406,7 @@ in
 
                 <para>
                   Use this if you want to run a private bridge, for
-                  example because you'll give out your bridge address
+                  example because you'll give out your bridge addr
                   manually to your friends.
                 </para>
 
@@ -440,269 +428,393 @@ in
           '';
         };
 
-        bridgeTransports = mkOption {
-          type = types.listOf types.str;
-          default = ["obfs4"];
-          example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
-          description = "List of pluggable transports";
-        };
-
-        nickname = mkOption {
-          type = types.str;
-          default = "anonymous";
-          description = ''
-            A unique handle for your TOR relay.
-          '';
-        };
-
-        contactInfo = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "admin@relay.com";
-          description = ''
-            Contact information for the relay owner (e.g. a mail
-            address and GPG key ID).
-          '';
-        };
-
-        accountingMax = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "450 GBytes";
-          description = ''
-            Specify maximum bandwidth allowed during an accounting period. This
-            allows you to limit overall tor bandwidth over some time period.
-            See the <literal>AccountingMax</literal> option by looking at the
-            tor manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-
-            Note this limit applies individually to upload and
-            download; if you specify <literal>"500 GBytes"</literal>
-            here, then you may transfer up to 1 TBytes of overall
-            bandwidth (500 GB upload, 500 GB download).
-          '';
-        };
-
-        accountingStart = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "month 1 1:00";
-          description = ''
-            Specify length of an accounting period. This allows you to limit
-            overall tor bandwidth over some time period. See the
-            <literal>AccountingStart</literal> option by looking at the tor
-            manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-          '';
-        };
-
-        bandwidthRate = mkOption {
-          type = types.nullOr types.int;
-          default = null;
-          example = 100;
-          description = ''
-            Specify this to limit the bandwidth usage of relayed (server)
-            traffic. Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        bandwidthBurst = mkOption {
-          type = types.nullOr types.int;
-          default = cfg.relay.bandwidthRate;
-          example = 200;
-          description = ''
-            Specify this to allow bursts of the bandwidth usage of relayed (server)
-            traffic. The average usage will still be as specified in relayBandwidthRate.
-            Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        address = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "noname.example.com";
-          description = ''
-            The IP address or full DNS name for advertised address of your relay.
-            Leave unset and Tor will guess.
-          '';
-        };
-
-        port = mkOption {
-          type    = types.either types.int types.str;
-          example = 143;
-          description = ''
-            What port to advertise for Tor connections. This corresponds to the
-            <literal>ORPort</literal> section in the Tor manual; see
-            <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more details.
-
-            At a minimum, you should just specify the port for the
-            relay to listen on; a common one like 143, 22, 80, or 443
-            to help Tor users who may have very restrictive port-based
-            firewalls.
-          '';
-        };
-
-        exitPolicy = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "accept *:6660-6667,reject *:*";
-          description = ''
-            A comma-separated list of exit policies. They're
-            considered first to last, and the first match wins. If you
-            want to _replace_ the default exit policy, end this with
-            either a reject *:* or an accept *:*. Otherwise, you're
-            _augmenting_ (prepending to) the default exit policy.
-            Leave commented to just use the default, which is
-            available in the man page or at
-            <link xlink:href="https://www.torproject.org/documentation.html" />.
-
-            Look at
-            <link xlink:href="https://www.torproject.org/faq-abuse.html#TypicalAbuses" />
-            for issues you might encounter if you use the default
-            exit policy.
-
-            If certain IPs and ports are blocked externally, e.g. by
-            your firewall, you should update your exit policy to
-            reflect this -- otherwise Tor users will be told that
-            those destinations are down.
-          '';
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "example.org/www" = {
+              map = [ 80 ];
+              authorizedClients = [
+                "descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+              ];
+            };
+          };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.path = mkOption {
+              type = types.path;
+              description = ''
+                Path where to store the data files of the hidden service.
+                If the <option>secretKey</option> is null
+                this defaults to <literal>${stateDir}/onion/$onion</literal>,
+                otherwise to <literal>${runDir}/onion/$onion</literal>.
+              '';
+            };
+            options.secretKey = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              example = "/run/keys/tor/onion/expyuzz4wqqyqhjn/hs_ed25519_secret_key";
+              description = ''
+                Secret key of the onion service.
+                If null, Tor reuses any preexisting secret key (in <option>path</option>)
+                or generates a new one.
+                The associated public key and hostname are deterministically regenerated
+                from this file if they do not exist.
+              '';
+            };
+            options.authorizeClient = mkOption {
+              description = descriptionGeneric "HiddenServiceAuthorizeClient";
+              default = null;
+              type = types.nullOr (types.submodule ({...}: {
+                options = {
+                  authType = mkOption {
+                    type = types.enum [ "basic" "stealth" ];
+                    description = ''
+                      Either <literal>"basic"</literal> for a general-purpose authorization protocol
+                      or <literal>"stealth"</literal> for a less scalable protocol
+                      that also hides service activity from unauthorized clients.
+                    '';
+                  };
+                  clientNames = mkOption {
+                    type = with types; nonEmptyListOf (strMatching "[A-Za-z0-9+-_]+");
+                    description = ''
+                      Only clients that are listed here are authorized to access the hidden service.
+                      Generated authorization data can be found in <filename>${stateDir}/onion/$name/hostname</filename>.
+                      Clients need to put this authorization data in their configuration file using
+                      <xref linkend="opt-services.tor.settings.HidServAuth"/>.
+                    '';
+                  };
+                };
+              }));
+            };
+            options.authorizedClients = mkOption {
+              description = ''
+                Authorized clients for a v3 onion service,
+                as a list of public key, in the format:
+                <screen>descriptor:x25519:&lt;base32-public-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf str;
+              default = [];
+              example = ["descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"];
+            };
+            options.map = mkOption {
+              description = descriptionGeneric "HiddenServicePort";
+              type = with types; listOf (oneOf [
+                port (submodule ({...}: {
+                  options = {
+                    port = optionPort;
+                    target = mkOption {
+                      default = null;
+                      type = nullOr (submodule ({...}: {
+                        options = {
+                          unix = optionUnix;
+                          addr = optionAddress;
+                          port = optionPort;
+                        };
+                      }));
+                    };
+                  };
+                }))
+              ]);
+              apply = map (v: if isInt v then {port=v; target=null;} else v);
+            };
+            options.version = mkOption {
+              description = descriptionGeneric "HiddenServiceVersion";
+              type = with types; nullOr (enum [2 3]);
+              default = null;
+            };
+            options.settings = mkOption {
+              description = ''
+                Settings of the onion service.
+              '' + descriptionGeneric "_hidden_service_options";
+              default = {};
+              type = types.submodule {
+                freeformType = with types;
+                  (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+                    description = "settings option";
+                  };
+                options.HiddenServiceAllowUnknownPorts = optionBool "HiddenServiceAllowUnknownPorts";
+                options.HiddenServiceDirGroupReadable = optionBool "HiddenServiceDirGroupReadable";
+                options.HiddenServiceExportCircuitID = mkOption {
+                  description = descriptionGeneric "HiddenServiceExportCircuitID";
+                  type = with types; nullOr (enum ["haproxy"]);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreams = mkOption {
+                  description = descriptionGeneric "HiddenServiceMaxStreams";
+                  type = with types; nullOr (ints.between 0 65535);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreamsCloseCircuit = optionBool "HiddenServiceMaxStreamsCloseCircuit";
+                options.HiddenServiceNumIntroductionPoints = mkOption {
+                  description = descriptionGeneric "HiddenServiceNumIntroductionPoints";
+                  type = with types; nullOr (ints.between 0 20);
+                  default = null;
+                };
+                options.HiddenServiceSingleHopMode = optionBool "HiddenServiceSingleHopMode";
+                options.RendPostPeriod = optionString "RendPostPeriod";
+              };
+            };
+            config = {
+              path = mkDefault ((if config.secretKey == null then stateDir else runDir) + "/onion/${name}");
+              settings.HiddenServiceVersion = config.version;
+              settings.HiddenServiceAuthorizeClient =
+                if config.authorizeClient != null then
+                  config.authorizeClient.authType + " " +
+                  concatStringsSep "," config.authorizeClient.clientNames
+                else null;
+              settings.HiddenServicePort = map (p: mkValueString "" p.port + " " + mkValueString "" p.target) config.map;
+            };
+          }));
         };
       };
 
-      hiddenServices = mkOption {
+      settings = mkOption {
         description = ''
-          A set of static hidden services that terminate their Tor
-          circuits at this node.
-
-          Every element in this set declares a virtual onion host.
-
-          You can specify your onion address by putting corresponding
-          private key to an appropriate place in ${torDirectory}.
-
-          For services without private keys in ${torDirectory} Tor
-          daemon will generate random key pairs (which implies random
-          onion addresses) on restart. The latter could take a while,
-          please be patient.
-
-          <note><para>
-            Hidden services can be useful even if you don't intend to
-            actually <emphasis>hide</emphasis> them, since they can
-            also be seen as a kind of NAT traversal mechanism.
-
-            E.g. the example will make your sshd, whatever runs on
-            "8080" and your mail server available from anywhere where
-            the Tor network is available (which, with the help from
-            bridges, is pretty much everywhere), even if both client
-            and server machines are behind NAT you have no control
-            over.
-          </para></note>
+          See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en">torrc manual</link>
+          for documentation.
         '';
         default = {};
-        example = literalExample ''
-          { "my-hidden-service-example".map = [
-              { port = 22; }                # map ssh port to this machine's ssh
-              { port = 80; toPort = 8080; } # map http port to whatever runs on 8080
-              { port = "sip"; toHost = "mail.example.com"; toPort = "imap"; } # because we can
-            ];
-          }
-        '';
-        type = types.attrsOf (types.submodule ({name, ...}: {
-          options = {
-
-             name = mkOption {
-               type = types.str;
-               description = ''
-                 Name of this tor hidden service.
-
-                 This is purely descriptive.
-
-                 After restarting Tor daemon you should be able to
-                 find your .onion address in
-                 <literal>${torDirectory}/onion/$name/hostname</literal>.
-               '';
-             };
-
-             map = mkOption {
-               default = [];
-               description = "Port mapping for this hidden service.";
-               type = types.listOf (types.submodule ({config, ...}: {
-                 options = {
-
-                   port = mkOption {
-                     type = types.either types.int types.str;
-                     example = 80;
-                     description = ''
-                       Hidden service port to "bind to".
-                     '';
-                   };
-
-                   destination = mkOption {
-                     internal = true;
-                     type = types.str;
-                     description = "Forward these connections where?";
-                   };
-
-                   toHost = mkOption {
-                     type = types.str;
-                     default = "127.0.0.1";
-                     description = "Mapping destination host.";
-                   };
-
-                   toPort = mkOption {
-                     type = types.either types.int types.str;
-                     example = 8080;
-                     description = "Mapping destination port.";
-                   };
-
-                 };
-
-                 config = {
-                   toPort = mkDefault config.port;
-                   destination = mkDefault "${config.toHost}:${toString config.toPort}";
-                 };
-               }));
-             };
-
-             authorizeClient = mkOption {
-               default = null;
-               description = "If configured, the hidden service is accessible for authorized clients only.";
-               type = types.nullOr (types.submodule ({...}: {
-
-                 options = {
-
-                   authType = mkOption {
-                     type = types.enum [ "basic" "stealth" ];
-                     description = ''
-                       Either <literal>"basic"</literal> for a general-purpose authorization protocol
-                       or <literal>"stealth"</literal> for a less scalable protocol
-                       that also hides service activity from unauthorized clients.
-                     '';
-                   };
-
-                   clientNames = mkOption {
-                     type = types.nonEmptyListOf (types.strMatching "[A-Za-z0-9+-_]+");
-                     description = ''
-                       Only clients that are listed here are authorized to access the hidden service.
-                       Generated authorization data can be found in <filename>${torDirectory}/onion/$name/hostname</filename>.
-                       Clients need to put this authorization data in their configuration file using <literal>HidServAuth</literal>.
-                     '';
-                   };
-                 };
-               }));
-             };
-
-             version = mkOption {
-               default = null;
-               description = "Rendezvous service descriptor version to publish for the hidden service. Currently, versions 2 and 3 are supported. (Default: 2)";
-               type = types.nullOr (types.enum [ 2 3 ]);
-             };
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+              description = "settings option";
+            };
+          options.Address = optionString "Address";
+          options.AssumeReachable = optionBool "AssumeReachable";
+          options.AccountingMax = optionBandwith "AccountingMax";
+          options.AccountingStart = optionString "AccountingStart";
+          options.AuthDirHasIPv6Connectivity = optionBool "AuthDirHasIPv6Connectivity";
+          options.AuthDirListBadExits = optionBool "AuthDirListBadExits";
+          options.AuthDirPinKeys = optionBool "AuthDirPinKeys";
+          options.AuthDirSharedRandomness = optionBool "AuthDirSharedRandomness";
+          options.AuthDirTestEd25519LinkKeys = optionBool "AuthDirTestEd25519LinkKeys";
+          options.AuthoritativeDirectory = optionBool "AuthoritativeDirectory";
+          options.AutomapHostsOnResolve = optionBool "AutomapHostsOnResolve";
+          options.AutomapHostsSuffixes = optionStrings "AutomapHostsSuffixes" // {
+            default = [".onion" ".exit"];
+            example = [".onion"];
           };
-
-          config = {
-            name = mkDefault name;
+          options.BandwidthBurst = optionBandwith "BandwidthBurst";
+          options.BandwidthRate = optionBandwith "BandwidthRate";
+          options.BridgeAuthoritativeDir = optionBool "BridgeAuthoritativeDir";
+          options.BridgeRecordUsageByCountry = optionBool "BridgeRecordUsageByCountry";
+          options.BridgeRelay = optionBool "BridgeRelay" // { default = false; };
+          options.CacheDirectory = optionPath "CacheDirectory";
+          options.CacheDirectoryGroupReadable = optionBool "CacheDirectoryGroupReadable"; # default is null and like "auto"
+          options.CellStatistics = optionBool "CellStatistics";
+          options.ClientAutoIPv6ORPort = optionBool "ClientAutoIPv6ORPort";
+          options.ClientDNSRejectInternalAddresses = optionBool "ClientDNSRejectInternalAddresses";
+          options.ClientOnionAuthDir = mkOption {
+            description = descriptionGeneric "ClientOnionAuthDir";
+            default = null;
+            type = with types; nullOr path;
+          };
+          options.ClientPreferIPv6DirPort = optionBool "ClientPreferIPv6DirPort"; # default is null and like "auto"
+          options.ClientPreferIPv6ORPort = optionBool "ClientPreferIPv6ORPort"; # default is null and like "auto"
+          options.ClientRejectInternalAddresses = optionBool "ClientRejectInternalAddresses";
+          options.ClientUseIPv4 = optionBool "ClientUseIPv4";
+          options.ClientUseIPv6 = optionBool "ClientUseIPv6";
+          options.ConnDirectionStatistics = optionBool "ConnDirectionStatistics";
+          options.ConstrainedSockets = optionBool "ConstrainedSockets";
+          options.ContactInfo = optionString "ContactInfo";
+          options.ControlPort = mkOption rec {
+            description = descriptionGeneric "ControlPort";
+            default = [];
+            example = [{port = 9051;}];
+            type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+              port (enum ["auto"]) (submodule ({config, ...}: let
+                flags = ["GroupWritable" "RelaxDirModeCheck" "WorldWritable"];
+                in {
+                options = {
+                  unix = optionUnix;
+                  flags = optionFlags;
+                  addr = optionAddress;
+                  port = optionPort;
+                } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+                config = {
+                  flags = filter (name: config.${name} == true) flags;
+                };
+              }))
+            ]))];
+          };
+          options.ControlPortFileGroupReadable= optionBool "ControlPortFileGroupReadable";
+          options.ControlPortWriteToFile = optionPath "ControlPortWriteToFile";
+          options.ControlSocket = optionPath "ControlSocket";
+          options.ControlSocketsGroupWritable = optionBool "ControlSocketsGroupWritable";
+          options.CookieAuthFile = optionPath "CookieAuthFile";
+          options.CookieAuthFileGroupReadable = optionBool "CookieAuthFileGroupReadable";
+          options.CookieAuthentication = optionBool "CookieAuthentication";
+          options.DataDirectory = optionPath "DataDirectory" // { default = stateDir; };
+          options.DataDirectoryGroupReadable = optionBool "DataDirectoryGroupReadable";
+          options.DirPortFrontPage = optionPath "DirPortFrontPage";
+          options.DirAllowPrivateAddresses = optionBool "DirAllowPrivateAddresses";
+          options.DormantCanceledByStartup = optionBool "DormantCanceledByStartup";
+          options.DormantOnFirstStartup = optionBool "DormantOnFirstStartup";
+          options.DormantTimeoutDisabledByIdleStreams = optionBool "DormantTimeoutDisabledByIdleStreams";
+          options.DirCache = optionBool "DirCache";
+          options.DirPolicy = mkOption {
+            description = descriptionGeneric "DirPolicy";
+            type = with types; listOf str;
+            default = [];
+            example = ["accept *:*"];
+          };
+          options.DirPort = optionORPort "DirPort";
+          options.DirReqStatistics = optionBool "DirReqStatistics";
+          options.DisableAllSwap = optionBool "DisableAllSwap";
+          options.DisableDebuggerAttachment = optionBool "DisableDebuggerAttachment";
+          options.DisableNetwork = optionBool "DisableNetwork";
+          options.DisableOOSCheck = optionBool "DisableOOSCheck";
+          options.DNSPort = optionIsolablePorts "DNSPort";
+          options.DoSCircuitCreationEnabled = optionBool "DoSCircuitCreationEnabled";
+          options.DoSConnectionEnabled = optionBool "DoSConnectionEnabled"; # default is null and like "auto"
+          options.DoSRefuseSingleHopClientRendezvous = optionBool "DoSRefuseSingleHopClientRendezvous";
+          options.DownloadExtraInfo = optionBool "DownloadExtraInfo";
+          options.EnforceDistinctSubnets = optionBool "EnforceDistinctSubnets";
+          options.EntryStatistics = optionBool "EntryStatistics";
+          options.ExitPolicy = optionStrings "ExitPolicy" // {
+            default = ["reject *:*"];
+            example = ["accept *:*"];
+          };
+          options.ExitPolicyRejectLocalInterfaces = optionBool "ExitPolicyRejectLocalInterfaces";
+          options.ExitPolicyRejectPrivate = optionBool "ExitPolicyRejectPrivate";
+          options.ExitPortStatistics = optionBool "ExitPortStatistics";
+          options.ExitRelay = optionBool "ExitRelay"; # default is null and like "auto"
+          options.ExtORPort = mkOption {
+            description = descriptionGeneric "ExtORPort";
+            default = null;
+            type = with types; nullOr (oneOf [
+              port (enum ["auto"]) (submodule ({...}: {
+                options = {
+                  addr = optionAddress;
+                  port = optionPort;
+                };
+              }))
+            ]);
+            apply = p: if isInt p || isString p then { port = p; } else p;
+          };
+          options.ExtORPortCookieAuthFile = optionPath "ExtORPortCookieAuthFile";
+          options.ExtORPortCookieAuthFileGroupReadable = optionBool "ExtORPortCookieAuthFileGroupReadable";
+          options.ExtendAllowPrivateAddresses = optionBool "ExtendAllowPrivateAddresses";
+          options.ExtraInfoStatistics = optionBool "ExtraInfoStatistics";
+          options.FascistFirewall = optionBool "FascistFirewall";
+          options.FetchDirInfoEarly = optionBool "FetchDirInfoEarly";
+          options.FetchDirInfoExtraEarly = optionBool "FetchDirInfoExtraEarly";
+          options.FetchHidServDescriptors = optionBool "FetchHidServDescriptors";
+          options.FetchServerDescriptors = optionBool "FetchServerDescriptors";
+          options.FetchUselessDescriptors = optionBool "FetchUselessDescriptors";
+          options.ReachableAddresses = optionStrings "ReachableAddresses";
+          options.ReachableDirAddresses = optionStrings "ReachableDirAddresses";
+          options.ReachableORAddresses = optionStrings "ReachableORAddresses";
+          options.GeoIPFile = optionPath "GeoIPFile";
+          options.GeoIPv6File = optionPath "GeoIPv6File";
+          options.GuardfractionFile = optionPath "GuardfractionFile";
+          options.HidServAuth = mkOption {
+            description = descriptionGeneric "HidServAuth";
+            default = [];
+            type = with types; listOf (oneOf [
+              (submodule {
+                options = {
+                  onion = mkOption {
+                    type = strMatching "[a-z2-7]{16}(\\.onion)?";
+                    description = "Onion address.";
+                    example = "xxxxxxxxxxxxxxxx.onion";
+                  };
+                  auth = mkOption {
+                    type = strMatching "[A-Za-z0-9+/]{22}";
+                    description = "Authentication cookie.";
+                  };
+                };
+              })
+            ]);
           };
-        }));
+          options.HiddenServiceNonAnonymousMode = optionBool "HiddenServiceNonAnonymousMode";
+          options.HiddenServiceStatistics = optionBool "HiddenServiceStatistics";
+          options.HSLayer2Nodes = optionStrings "HSLayer2Nodes";
+          options.HSLayer3Nodes = optionStrings "HSLayer3Nodes";
+          options.HTTPTunnelPort = optionIsolablePorts "HTTPTunnelPort";
+          options.IPv6Exit = optionBool "IPv6Exit";
+          options.KeyDirectory = optionPath "KeyDirectory";
+          options.KeyDirectoryGroupReadable = optionBool "KeyDirectoryGroupReadable";
+          options.LogMessageDomains = optionBool "LogMessageDomains";
+          options.LongLivedPorts = optionPorts "LongLivedPorts";
+          options.MainloopStats = optionBool "MainloopStats";
+          options.MaxAdvertisedBandwidth = optionBandwith "MaxAdvertisedBandwidth";
+          options.MaxCircuitDirtiness = optionInt "MaxCircuitDirtiness";
+          options.MaxClientCircuitsPending = optionInt "MaxClientCircuitsPending";
+          options.NATDPort = optionIsolablePorts "NATDPort";
+          options.NewCircuitPeriod = optionInt "NewCircuitPeriod";
+          options.Nickname = optionString "Nickname";
+          options.ORPort = optionORPort "ORPort";
+          options.OfflineMasterKey = optionBool "OfflineMasterKey";
+          options.OptimisticData = optionBool "OptimisticData"; # default is null and like "auto"
+          options.PaddingStatistics = optionBool "PaddingStatistics";
+          options.PerConnBWBurst = optionBandwith "PerConnBWBurst";
+          options.PerConnBWRate = optionBandwith "PerConnBWRate";
+          options.PidFile = optionPath "PidFile";
+          options.ProtocolWarnings = optionBool "ProtocolWarnings";
+          options.PublishHidServDescriptors = optionBool "PublishHidServDescriptors";
+          options.PublishServerDescriptor = mkOption {
+            description = descriptionGeneric "PublishServerDescriptor";
+            type = with types; nullOr (enum [false true 0 1 "0" "1" "v3" "bridge"]);
+            default = null;
+          };
+          options.ReducedExitPolicy = optionBool "ReducedExitPolicy";
+          options.RefuseUnknownExits = optionBool "RefuseUnknownExits"; # default is null and like "auto"
+          options.RejectPlaintextPorts = optionPorts "RejectPlaintextPorts";
+          options.RelayBandwidthBurst = optionBandwith "RelayBandwidthBurst";
+          options.RelayBandwidthRate = optionBandwith "RelayBandwidthRate";
+          #options.RunAsDaemon
+          options.Sandbox = optionBool "Sandbox";
+          options.ServerDNSAllowBrokenConfig = optionBool "ServerDNSAllowBrokenConfig";
+          options.ServerDNSAllowNonRFC953Hostnames = optionBool "ServerDNSAllowNonRFC953Hostnames";
+          options.ServerDNSDetectHijacking = optionBool "ServerDNSDetectHijacking";
+          options.ServerDNSRandomizeCase = optionBool "ServerDNSRandomizeCase";
+          options.ServerDNSResolvConfFile = optionPath "ServerDNSResolvConfFile";
+          options.ServerDNSSearchDomains = optionBool "ServerDNSSearchDomains";
+          options.ServerTransportPlugin = mkOption {
+            description = descriptionGeneric "ServerTransportPlugin";
+            default = null;
+            type = with types; nullOr (submodule ({...}: {
+              options = {
+                transports = mkOption {
+                  description = "List of pluggable transports.";
+                  type = listOf str;
+                  example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
+                };
+                exec = mkOption {
+                  type = types.str;
+                  description = "Command of pluggable transport.";
+                };
+              };
+            }));
+          };
+          options.SocksPolicy = optionStrings "SocksPolicy" // {
+            example = ["accept *:*"];
+          };
+          options.SOCKSPort = mkOption {
+            description = descriptionGeneric "SOCKSPort";
+            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            example = [{port = 9090;}];
+            type = types.listOf (optionSOCKSPort true);
+          };
+          options.TestingTorNetwork = optionBool "TestingTorNetwork";
+          options.TransPort = optionIsolablePorts "TransPort";
+          options.TransProxyType = mkOption {
+            description = descriptionGeneric "TransProxyType";
+            type = with types; nullOr (enum ["default" "TPROXY" "ipfw" "pf-divert"]);
+            default = null;
+          };
+          #options.TruncateLogFile
+          options.UnixSocksGroupWritable = optionBool "UnixSocksGroupWritable";
+          options.UseDefaultFallbackDirs = optionBool "UseDefaultFallbackDirs";
+          options.UseMicrodescriptors = optionBool "UseMicrodescriptors";
+          options.V3AuthUseLegacyKey = optionBool "V3AuthUseLegacyKey";
+          options.V3AuthoritativeDirectory = optionBool "V3AuthoritativeDirectory";
+          options.VersioningAuthoritativeDirectory = optionBool "VersioningAuthoritativeDirectory";
+          options.VirtualAddrNetworkIPv4 = optionString "VirtualAddrNetworkIPv4";
+          options.VirtualAddrNetworkIPv6 = optionString "VirtualAddrNetworkIPv6";
+          options.WarnPlaintextPorts = optionPorts "WarnPlaintextPorts";
+        };
       };
     };
   };
@@ -710,90 +822,219 @@ in
   config = mkIf cfg.enable {
     # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
     # sends a lot of stats
-    warnings = optional (cfg.relay.enable && cfg.hiddenServices != {})
+    warnings = optional (cfg.settings.BridgeRelay &&
+      flatten (mapAttrsToList (n: o: o.map) cfg.relay.onionServices) != [])
       ''
         Running Tor hidden services on a public relay makes the
         presence of hidden services visible through simple statistical
         analysis of publicly available data.
+        See https://trac.torproject.org/projects/tor/ticket/8742
 
         You can safely ignore this warning if you don't intend to
         actually hide your hidden services. In either case, you can
         always create a container/VM with a separate Tor daemon instance.
-      '';
+      '' ++
+      flatten (mapAttrsToList (n: o:
+        optional (o.settings.HiddenServiceVersion == 2) [
+          (optional (o.settings.HiddenServiceExportCircuitID != null) ''
+            HiddenServiceExportCircuitID is used in the HiddenService: ${n}
+            but this option is only for v3 hidden services.
+          '')
+        ] ++
+        optional (o.settings.HiddenServiceVersion != 2) [
+          (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
+            HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+          (optional (o.settings.RendPostPeriod != null) ''
+            RendPostPeriod is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+        ]
+      ) cfg.relay.onionServices);
 
     users.groups.tor.gid = config.ids.gids.tor;
     users.users.tor =
       { description = "Tor Daemon User";
         createHome  = true;
-        home        = torDirectory;
+        home        = stateDir;
         group       = "tor";
         uid         = config.ids.uids.tor;
       };
 
-    # We have to do this instead of using RuntimeDirectory option in
-    # the service below because systemd has no way to set owners of
-    # RuntimeDirectory and putting this into the service below
-    # requires that service to relax it's sandbox since this needs
-    # writable /run
-    systemd.services.tor-init =
-      { description = "Tor Daemon Init";
-        wantedBy = [ "tor.service" ];
-        script = ''
-          install -m 0700 -o tor -g tor -d ${torDirectory} ${torDirectory}/onion
-          install -m 0750 -o tor -g tor -d ${torRunDirectory}
-        '';
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-        };
-      };
+    services.tor.settings = mkMerge [
+      (mkIf cfg.enableGeoIP {
+        GeoIPFile = "${cfg.package.geoip}/share/tor/geoip";
+        GeoIPv6File = "${cfg.package.geoip}/share/tor/geoip6";
+      })
+      (mkIf cfg.controlSocket.enable {
+        ControlPort = [ { unix = runDir + "/control"; GroupWritable=true; RelaxDirModeCheck=true; } ];
+      })
+      (mkIf cfg.relay.enable (
+        optionalAttrs (cfg.relay.role != "exit") {
+          ExitPolicy = mkForce ["reject *:*"];
+        } //
+        optionalAttrs (elem cfg.relay.role ["bridge" "private-bridge"]) {
+          BridgeRelay = true;
+          ExtORPort.port = mkDefault "auto";
+          ServerTransportPlugin.transports = mkDefault ["obfs4"];
+          ServerTransportPlugin.exec = mkDefault "${pkgs.obfs4}/bin/obfs4proxy managed";
+        } // optionalAttrs (cfg.relay.role == "private-bridge") {
+          ExtraInfoStatistics = false;
+          PublishServerDescriptor = false;
+        }
+      ))
+      (mkIf (!cfg.relay.enable) {
+        # Avoid surprises when leaving ORPort/DirPort configurations in cfg.settings,
+        # because it would still enable Tor as a relay,
+        # which can trigger all sort of problems when not carefully done,
+        # like the blocklisting of the machine's IP addresses
+        # by some hosting providers...
+        DirPort = mkForce [];
+        ORPort = mkForce [];
+        PublishServerDescriptor = mkForce false;
+      })
+      (mkIf cfg.client.enable (
+        { SOCKSPort = [ cfg.client.socksListenAddress ];
+        } // optionalAttrs cfg.client.transparentProxy.enable {
+          TransPort = [{ addr = "127.0.0.1"; port = 9040; }];
+        } // optionalAttrs cfg.client.dns.enable {
+          DNSPort = [{ addr = "127.0.0.1"; port = 9053; }];
+          AutomapHostsOnResolve = true;
+        } // optionalAttrs (flatten (mapAttrsToList (n: o: o.clientAuthorizations) cfg.client.onionServices) != []) {
+          ClientOnionAuthDir = runDir + "/ClientOnionAuthDir";
+        }
+      ))
+    ];
 
-    systemd.services.tor =
-      { description = "Tor Daemon";
-        path = [ pkgs.tor ];
-
-        wantedBy = [ "multi-user.target" ];
-        after    = [ "tor-init.service" "network.target" ];
-        restartTriggers = [ torRcFile ];
-
-        serviceConfig =
-          { Type         = "simple";
-            # Translated from the upstream contrib/dist/tor.service.in
-            ExecStartPre = "${cfg.package}/bin/tor -f ${torRcFile} --verify-config";
-            ExecStart    = "${cfg.package}/bin/tor -f ${torRcFile}";
-            ExecReload   = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-            KillSignal   = "SIGINT";
-            TimeoutSec   = 30;
-            Restart      = "on-failure";
-            LimitNOFILE  = 32768;
-
-            # Hardening
-            # this seems to unshare /run despite what systemd.exec(5) says
-            PrivateTmp              = mkIf (!cfg.controlSocket.enable) "yes";
-            PrivateDevices          = "yes";
-            ProtectHome             = "yes";
-            ProtectSystem           = "strict";
-            InaccessiblePaths       = "/home";
-            ReadOnlyPaths           = "/";
-            ReadWritePaths          = [ torDirectory torRunDirectory ];
-            NoNewPrivileges         = "yes";
-
-            # tor.service.in has this in, but this line it fails to spawn a namespace when using hidden services
-            #CapabilityBoundingSet   = "CAP_SETUID CAP_SETGID CAP_NET_BIND_SERVICE";
-          };
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts =
+        concatMap (o:
+          if isInt o && o > 0 then [o]
+          else if o ? "port" && isInt o.port && o.port > 0 then [o.port]
+          else []
+        ) (flatten [
+          cfg.settings.ORPort
+          cfg.settings.DirPort
+        ]);
+    };
+
+    systemd.services.tor = {
+      description = "Tor Daemon";
+      path = [ pkgs.tor ];
+
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      restartTriggers = [ torrc ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "tor";
+        Group = "tor";
+        ExecStartPre = [
+          "${cfg.package}/bin/tor -f ${torrc} --verify-config"
+          # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
+          ("+" + pkgs.writeShellScript "ExecStartPre" (concatStringsSep "\n" (flatten (["set -eu"] ++
+            mapAttrsToList (name: onion:
+              optional (onion.authorizedClients != []) ''
+                rm -rf ${escapeShellArg onion.path}/authorized_clients
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path} ${escapeShellArg onion.path}/authorized_clients
+              '' ++
+              imap0 (i: pubKey: ''
+                echo ${pubKey} |
+                install -o tor -g tor -m 0400 /dev/stdin ${escapeShellArg onion.path}/authorized_clients/${toString i}.auth
+              '') onion.authorizedClients ++
+              optional (onion.secretKey != null) ''
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path}
+                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey})"
+                case "$key" in
+                 ("== ed25519v"*"-secret")
+                  install -o tor -g tor -m 0400 ${escapeShellArg onion.secretKey} ${escapeShellArg onion.path}/hs_ed25519_secret_key;;
+                 (*) echo >&2 "NixOS does not (yet) support secret key type for onion: ${name}"; exit 1;;
+                esac
+              ''
+            ) cfg.relay.onionServices ++
+            mapAttrsToList (name: onion: imap0 (i: prvKeyPath:
+              let hostname = removeSuffix ".onion" name; in ''
+              printf "%s:" ${escapeShellArg hostname} | cat - ${escapeShellArg prvKeyPath} |
+              install -o tor -g tor -m 0700 /dev/stdin \
+               ${runDir}/ClientOnionAuthDir/${escapeShellArg hostname}.${toString i}.auth_private
+            '') onion.clientAuthorizations)
+            cfg.client.onionServices
+          ))))
+        ];
+        ExecStart = "${cfg.package}/bin/tor -f ${torrc}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutSec = 30;
+        Restart = "on-failure";
+        LimitNOFILE = 32768;
+        RuntimeDirectory = [
+          # g+x allows access to the control socket
+          "tor"
+          "tor/root"
+          # g+x can't be removed in ExecStart=, but will be removed by Tor
+          "tor/ClientOnionAuthDir"
+        ];
+        RuntimeDirectoryMode = "0710";
+        StateDirectoryMode = "0700";
+        StateDirectory = [
+            "tor"
+            "tor/onion"
+          ] ++
+          flatten (mapAttrsToList (name: onion:
+            optional (onion.secretKey == null) "tor/onion/${name}"
+          ) cfg.relay.onionServices);
+        # The following options are only to optimize:
+        # systemd-analyze security tor
+        RootDirectory = runDir + "/root";
+        RootDirectoryStartOnly = true;
+        #InaccessiblePaths = [ "-+${runDir}/root" ];
+        UMask = "0066";
+        BindPaths = [ stateDir ];
+        BindReadOnlyPaths = [ storeDir "/etc" ];
+        AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # Tor cannot currently bind privileged port when PrivateUsers=true,
+        # see https://gitlab.torproject.org/legacy/trac/-/issues/20930
+        PrivateUsers = !bindsPrivilegedPort;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        # See also the finer but experimental option settings.Sandbox
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' tor
+          # in tests, and seem likely not necessary for tor.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
       };
+    };
 
     environment.systemPackages = [ cfg.package ];
-
-    services.privoxy = mkIf (cfg.client.enable && cfg.client.privoxy.enable) {
-      enable = true;
-      extraConfig = ''
-        forward-socks4a / ${cfg.client.socksListenAddressFaster} .
-        toggle  1
-        enable-remote-toggle 0
-        enable-edit-actions 0
-        enable-remote-http-toggle 0
-      '';
-    };
   };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
 }
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
index 16a90da52314e..4cdb3a041b59d 100644
--- a/nixos/modules/services/security/usbguard.nix
+++ b/nixos/modules/services/security/usbguard.nix
@@ -19,13 +19,13 @@ let
     PresentDevicePolicy=${cfg.presentDevicePolicy}
     PresentControllerPolicy=${cfg.presentControllerPolicy}
     InsertedDevicePolicy=${cfg.insertedDevicePolicy}
-    RestoreControllerDeviceState=${if cfg.restoreControllerDeviceState then "true" else "false"}
+    RestoreControllerDeviceState=${boolToString cfg.restoreControllerDeviceState}
     # 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"}
+    DeviceRulesWithPort=${boolToString cfg.deviceRulesWithPort}
     # HACK: that way audit logs still land in the journal
     AuditFilePath=/dev/null
   '';
@@ -173,7 +173,7 @@ in
 
       serviceConfig = {
         Type = "simple";
-        ExecStart = ''${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}'';
+        ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
         Restart = "on-failure";
 
         StateDirectory = [
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index 6a8a3a93327eb..5a20f6413b1b7 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -27,6 +27,11 @@ let
       ''}
     ${cfg.extraConfig}
   '';
+
+  allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
+
+  configOptions = escapeShellArgs (concatMap (p: ["-config" p]) allConfigPaths);
+
 in
 
 {
@@ -84,7 +89,14 @@ in
       storageConfig = mkOption {
         type = types.nullOr types.lines;
         default = null;
-        description = "Storage configuration";
+        description = ''
+          HCL configuration to insert in the storageBackend section.
+
+          Confidential values should not be specified here because this option's
+          value is written to the Nix store, which is publicly readable.
+          Provide credentials and such in a separate file using
+          <xref linkend="opt-services.vault.extraSettingsPaths"/>.
+        '';
       };
 
       telemetryConfig = mkOption {
@@ -98,6 +110,36 @@ in
         default = "";
         description = "Extra text appended to <filename>vault.hcl</filename>.";
       };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Configuration files to load besides the immutable one defined by the NixOS module.
+          This can be used to avoid putting credentials in the Nix store, which can be read by any user.
+
+          Each path can point to a JSON- or HCL-formatted file, or a directory
+          to be scanned for files with <literal>.hcl</literal> or
+          <literal>.json</literal> extensions.
+
+          To upload the confidential file with NixOps, use for example:
+
+          <programlisting><![CDATA[
+          # https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
+          deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
+            text = ${"''"}
+              storage "postgresql" {
+                connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
+              }
+            ${"''"};
+            user = "vault";
+          };
+          services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
+          services.vault.storageBackend = "postgresql";
+          users.users.vault.extraGroups = ["keys"];
+          ]]></programlisting>
+        '';
+      };
     };
   };
 
@@ -131,10 +173,12 @@ in
 
       restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
 
+      startLimitIntervalSec = 60;
+      startLimitBurst = 3;
       serviceConfig = {
         User = "vault";
         Group = "vault";
-        ExecStart = "${cfg.package}/bin/vault server -config ${configFile}";
+        ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
         ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
         PrivateDevices = true;
         PrivateTmp = true;
@@ -145,8 +189,6 @@ in
         KillSignal = "SIGINT";
         TimeoutStopSec = "30s";
         Restart = "on-failure";
-        StartLimitInterval = "60s";
-        StartLimitBurst = 3;
       };
 
       unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index 15fe822aec679..f83db30c1f02f 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -9,7 +9,7 @@ let cfg = config.services.cloud-init;
       nettools
       openssh
       shadow
-      utillinux
+      util-linux
     ] ++ optional cfg.btrfs.enable btrfs-progs
       ++ optional cfg.ext4.enable e2fsprogs
     ;
@@ -98,7 +98,7 @@ in
            - final-message
            - power-state-change
           '';
-        description = ''cloud-init configuration.'';
+        description = "cloud-init configuration.";
       };
 
     };
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index 4a60fec1ca806..d4cacb85694b9 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -1,6 +1,6 @@
 # D-Bus configuration and system bus daemon.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -11,6 +11,7 @@ let
   homeDir = "/run/dbus";
 
   configDir = pkgs.makeDBusConf {
+    inherit (cfg) apparmor;
     suidHelper = "${config.security.wrapperDir}/dbus-daemon-launch-helper";
     serviceDirectories = cfg.packages;
   };
@@ -18,7 +19,6 @@ let
 in
 
 {
-
   ###### interface
 
   options = {
@@ -52,11 +52,26 @@ in
         '';
       };
 
+      apparmor = mkOption {
+        type = types.enum [ "enabled" "disabled" "required" ];
+        description = ''
+          AppArmor mode for dbus.
+
+          <literal>enabled</literal> enables mediation when it's
+          supported in the kernel, <literal>disabled</literal>
+          always disables AppArmor even with kernel support, and
+          <literal>required</literal> fails when AppArmor was not found
+          in the kernel.
+        '';
+        default = "disabled";
+      };
+
       socketActivated = mkOption {
-        type = types.bool;
-        default = false;
+        type = types.nullOr types.bool;
+        default = null;
+        visible = false;
         description = ''
-          Make the user instance socket activated.
+          Removed option, do not use.
         '';
       };
     };
@@ -65,6 +80,14 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    warnings = optional (cfg.socketActivated != null) (
+      let
+        files = showFiles options.services.dbus.socketActivated.files;
+      in
+        "The option 'services.dbus.socketActivated' in ${files} no longer has"
+        + " any effect and can be safely removed: the user D-Bus session is"
+        + " now always socket activated."
+    );
 
     environment.systemPackages = [ pkgs.dbus.daemon pkgs.dbus ];
 
@@ -108,7 +131,7 @@ in
         reloadIfChanged = true;
         restartTriggers = [ configDir ];
       };
-      sockets.dbus.wantedBy = mkIf cfg.socketActivated [ "sockets.target" ];
+      sockets.dbus.wantedBy = [ "sockets.target" ];
     };
 
     environment.pathsToLink = [ "/etc/dbus-1" "/share/dbus-1" ];
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
index 45398cb261387..7ca4fdcf64d40 100644
--- a/nixos/modules/services/torrent/deluge.nix
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -41,6 +41,7 @@ in {
 
         openFilesLimit = mkOption {
           default = openFilesLimit;
+          type = types.either types.int types.str;
           description = ''
             Number of files to allow deluged to open.
           '';
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 014a22bb5a8d6..7bec073e26f71 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -197,7 +197,7 @@ in
           install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
            '${cfg.home}/${settingsDir}/settings.json'
         '')];
-        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f";
+        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f -g ${cfg.home}/${settingsDir}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = cfg.user;
         Group = cfg.group;
@@ -236,6 +236,7 @@ in
           # an AppArmor profile is provided to get a confinement based upon paths and rights.
           builtins.storeDir
           "/etc"
+          "/run"
           ] ++
           optional (cfg.settings.script-torrent-done-enabled &&
                     cfg.settings.script-torrent-done-filename != "")
@@ -396,9 +397,9 @@ in
           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.util-linuxMinimal.out}/lib/libblkid.so*,
+          mr ${getLib pkgs.util-linuxMinimal.out}/lib/libmount.so*,
+          mr ${getLib pkgs.util-linuxMinimal.out}/lib/libuuid.so*,
           mr ${getLib pkgs.xz}/lib/liblzma*.so*,
           mr ${getLib pkgs.zlib}/lib/libz*.so*,
 
@@ -408,6 +409,7 @@ in
           #r @{PROC}/@{pid}/environ,
           r @{PROC}/@{pid}/mounts,
           rwk /tmp/tr_session_id_*,
+          r /run/systemd/resolve/stub-resolv.conf,
 
           r ${pkgs.openssl.out}/etc/**,
           r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
diff --git a/nixos/modules/services/ttys/agetty.nix b/nixos/modules/services/ttys/getty.nix
index f3a629f7af700..ecfabef5fb131 100644
--- a/nixos/modules/services/ttys/agetty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -3,9 +3,19 @@
 with lib;
 
 let
+  cfg = config.services.getty;
 
-  autologinArg = optionalString (config.services.mingetty.autologinUser != null) "--autologin ${config.services.mingetty.autologinUser}";
-  gettyCmd = extraArgs: "@${pkgs.utillinux}/sbin/agetty agetty --login-program ${pkgs.shadow}/bin/login ${autologinArg} ${extraArgs}";
+  loginArgs = [
+    "--login-program" "${pkgs.shadow}/bin/login"
+  ] ++ optionals (cfg.autologinUser != null) [
+    "--autologin" cfg.autologinUser
+  ] ++ optionals (cfg.loginOptions != null) [
+    "--login-options" cfg.loginOptions
+  ];
+
+  gettyCmd = extraArgs:
+    "@${pkgs.util-linux}/sbin/agetty agetty ${escapeShellArgs loginArgs} "
+      + extraArgs;
 
 in
 
@@ -13,9 +23,13 @@ in
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "mingetty" ] [ "services" "getty" ])
+  ];
+
   options = {
 
-    services.mingetty = {
+    services.getty = {
 
       autologinUser = mkOption {
         type = types.nullOr types.str;
@@ -26,10 +40,27 @@ in
         '';
       };
 
+      loginOptions = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Template for arguments to be passed to
+          <citerefentry><refentrytitle>login</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry>.
+
+          See <citerefentry><refentrytitle>agetty</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry> for details,
+          including security considerations.  If unspecified, agetty
+          will not be invoked with a <option>--login-options</option>
+          option.
+        '';
+        example = "-h darkstar -- \u";
+      };
+
       greetingLine = mkOption {
         type = types.str;
         description = ''
-          Welcome line printed by mingetty.
+          Welcome line printed by agetty.
           The default shows current NixOS version label, machine type and tty.
         '';
       };
@@ -38,7 +69,7 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Help line printed by mingetty below the welcome line.
+          Help line printed by agetty below the welcome line.
           Used by the installation CD to give some hints on
           how to proceed.
         '';
@@ -65,7 +96,7 @@ in
   config = {
     # Note: this is set here rather than up there so that changing
     # nixos.label would not rebuild manual pages
-    services.mingetty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
+    services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
 
     systemd.services."getty@" =
       { serviceConfig.ExecStart = [
@@ -76,7 +107,7 @@ in
       };
 
     systemd.services."serial-getty@" =
-      let speeds = concatStringsSep "," (map toString config.services.mingetty.serialSpeed); in
+      let speeds = concatStringsSep "," (map toString config.services.getty.serialSpeed); in
       { serviceConfig.ExecStart = [
           "" # override upstream default with an empty ExecStart
           (gettyCmd "%I ${speeds} $TERM")
@@ -106,8 +137,8 @@ in
       { # Friendly greeting on the virtual consoles.
         source = pkgs.writeText "issue" ''
 
-          ${config.services.mingetty.greetingLine}
-          ${config.services.mingetty.helpLine}
+          ${config.services.getty.greetingLine}
+          ${config.services.getty.helpLine}
 
         '';
       };
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
index dc37f9bee4b36..4fe720bf044bc 100644
--- a/nixos/modules/services/ttys/kmscon.nix
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -82,11 +82,8 @@ in {
       X-RestartIfChanged=false
     '';
 
-    systemd.units."autovt@.service".unit = pkgs.runCommand "unit" { preferLocalBuild = true; }
-        ''
-          mkdir -p $out
-          ln -s ${config.systemd.units."kmsconvt@.service".unit}/kmsconvt@.service $out/autovt@.service
-        '';
+    systemd.suppressedSystemUnits = [ "autovt@.service" ];
+    systemd.units."kmsconvt@.service".aliases = [ "autovt@.service" ];
 
     systemd.services.systemd-vconsole-setup.enable = false;
 
diff --git a/nixos/modules/services/video/epgstation/generate b/nixos/modules/services/video/epgstation/generate
deleted file mode 100755
index 2940768b6d2c8..0000000000000
--- a/nixos/modules/services/video/epgstation/generate
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env -S nix-build --no-out-link
-
-# Script to generate default streaming configurations for EPGStation. There's
-# no need to run this script directly since generate.sh in the EPGStation
-# package directory would run this script for you.
-#
-# Usage: ./generate | xargs cat > streaming.json
-
-{ pkgs ? (import ../../../../.. {}) }:
-
-let
-  sampleConfigPath = "${pkgs.epgstation.src}/config/config.sample.json";
-  sampleConfig = builtins.fromJSON (builtins.readFile sampleConfigPath);
-  streamingConfig = {
-    inherit (sampleConfig)
-      mpegTsStreaming
-      mpegTsViewer
-      liveHLS
-      liveMP4
-      liveWebM
-      recordedDownloader
-      recordedStreaming
-      recordedViewer
-      recordedHLS;
-  };
-in
-pkgs.runCommand "streaming.json" { nativeBuildInputs = [ pkgs.jq ]; } ''
-  jq . <<<'${builtins.toJSON streamingConfig}' > $out
-''
-
-# vim:set ft=nix:
diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json
index 37957f6cb6a22..8eb99cf85584b 100644
--- a/nixos/modules/services/video/epgstation/streaming.json
+++ b/nixos/modules/services/video/epgstation/streaming.json
@@ -1,119 +1,119 @@
 {
   "liveHLS": [
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%",
-      "name": "720p"
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
     },
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%",
-      "name": "480p"
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
     },
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%",
-      "name": "180p"
+      "name": "180p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%"
     }
   ],
   "liveMP4": [
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
-      "name": "720p"
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
     },
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
-      "name": "480p"
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
     }
   ],
   "liveWebM": [
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
-      "name": "720p"
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
     },
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
-      "name": "480p"
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
     }
   ],
   "mpegTsStreaming": [
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1",
-      "name": "720p"
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
     },
     {
-      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1",
-      "name": "480p"
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
     },
     {
       "name": "Original"
     }
   ],
   "mpegTsViewer": {
-    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end",
-    "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS"
+    "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS",
+    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
   },
   "recordedDownloader": {
-    "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end",
-    "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME"
+    "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME",
+    "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end"
   },
-  "recordedHLS": [
-    {
-      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%",
-      "name": "720p"
-    },
-    {
-      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%",
-      "name": "480p"
-    },
-    {
-      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%",
-      "name": "480p(h265)"
-    }
-  ],
   "recordedStreaming": {
-    "mp4": [
+    "webm": [
       {
-        "ab": "192k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
         "name": "720p",
-        "vb": "3000k"
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
       },
       {
-        "ab": "128k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
         "name": "360p",
-        "vb": "1500k"
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
       }
     ],
-    "mpegTs": [
+    "mp4": [
       {
-        "ab": "192k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
-        "name": "720p (H.264)",
-        "vb": "3000k"
+        "name": "720p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
       },
       {
-        "ab": "128k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
-        "name": "360p (H.264)",
-        "vb": "1500k"
+        "name": "360p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
       }
     ],
-    "webm": [
+    "mpegTs": [
       {
-        "ab": "192k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
-        "name": "720p",
-        "vb": "3000k"
+        "name": "720p (H.264)",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
       },
       {
-        "ab": "128k",
-        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
-        "name": "360p",
-        "vb": "1500k"
+        "name": "360p (H.264)",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
       }
     ]
   },
+  "recordedHLS": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "480p(h265)",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%"
+    }
+  ],
   "recordedViewer": {
-    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end",
-    "ios": "infuse://x-callback-url/play?url=http://ADDRESS"
+    "ios": "infuse://x-callback-url/play?url=http://ADDRESS",
+    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
   }
 }
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index d9ebb3a98808c..9567223ebc7b5 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -336,7 +336,7 @@ in
         locations."/" = {
           priority = 1;
           index = "doku.php";
-          extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
+          extraConfig = "try_files $uri $uri/ @dokuwiki;";
         };
 
         locations."@dokuwiki" = {
diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix
index 899582a203040..2e755ae9d5233 100644
--- a/nixos/modules/services/web-apps/engelsystem.nix
+++ b/nixos/modules/services/web-apps/engelsystem.nix
@@ -10,7 +10,7 @@ in {
         default = false;
         example = true;
         description = ''
-          Whether to enable engelsystem, an online tool for coordinating helpers
+          Whether to enable engelsystem, an online tool for coordinating volunteers
           and shifts on large events.
         '';
         type = lib.types.bool;
diff --git a/nixos/modules/services/web-apps/frab.nix b/nixos/modules/services/web-apps/frab.nix
deleted file mode 100644
index 1b5890d6b0c73..0000000000000
--- a/nixos/modules/services/web-apps/frab.nix
+++ /dev/null
@@ -1,222 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.frab;
-
-  package = pkgs.frab;
-
-  databaseConfig = builtins.toJSON { production = cfg.database; };
-
-  frabEnv = {
-    RAILS_ENV = "production";
-    RACK_ENV = "production";
-    SECRET_KEY_BASE = cfg.secretKeyBase;
-    FRAB_HOST = cfg.host;
-    FRAB_PROTOCOL = cfg.protocol;
-    FROM_EMAIL = cfg.fromEmail;
-    RAILS_SERVE_STATIC_FILES = "1";
-  } // cfg.extraEnvironment;
-
-  frab-rake = pkgs.stdenv.mkDerivation {
-    name = "frab-rake";
-    buildInputs = [ package.env pkgs.makeWrapper ];
-    phases = "installPhase fixupPhase";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${package.env}/bin/bundle $out/bin/frab-bundle \
-          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") frabEnv)} \
-          --set PATH '${lib.makeBinPath (with pkgs; [ nodejs file imagemagick ])}:$PATH' \
-          --set RAKEOPT '-f ${package}/share/frab/Rakefile' \
-          --run 'cd ${package}/share/frab'
-      makeWrapper $out/bin/frab-bundle $out/bin/frab-rake \
-          --add-flags "exec rake"
-     '';
-  };
-
-in
-
-{
-  options = {
-    services.frab = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable the frab service.
-        '';
-      };
-
-      host = mkOption {
-        type = types.str;
-        example = "frab.example.com";
-        description = ''
-          Hostname under which this frab instance can be reached.
-        '';
-      };
-
-      protocol = mkOption {
-        type = types.str;
-        default = "https";
-        example = "http";
-        description = ''
-          Either http or https, depending on how your Frab instance
-          will be exposed to the public.
-        '';
-      };
-
-      fromEmail = mkOption {
-        type = types.str;
-        default = "frab@localhost";
-        description = ''
-          Email address used by frab.
-        '';
-      };
-
-      listenAddress = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = ''
-          Address or hostname frab should listen on.
-        '';
-      };
-
-      listenPort = mkOption {
-        type = types.int;
-        default = 3000;
-        description = ''
-          Port frab should listen on.
-        '';
-      };
-
-      statePath = mkOption {
-        type = types.str;
-        default = "/var/lib/frab";
-        description = ''
-          Directory where frab keeps its state.
-        '';
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "frab";
-        description = ''
-          User to run frab.
-        '';
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "frab";
-        description = ''
-          Group to run frab.
-        '';
-      };
-
-      secretKeyBase = mkOption {
-        type = types.str;
-        description = ''
-          Your secret key is used for verifying the integrity of signed cookies.
-          If you change this key, all old signed cookies will become invalid!
-
-          Make sure the secret is at least 30 characters and all random,
-          no regular words or you'll be exposed to dictionary attacks.
-        '';
-      };
-
-      database = mkOption {
-        type = types.attrs;
-        default = {
-          adapter = "sqlite3";
-          database = "/var/lib/frab/db.sqlite3";
-          pool = 5;
-          timeout = 5000;
-        };
-        example = {
-          adapter = "postgresql";
-          database = "frab";
-          host = "localhost";
-          username = "frabuser";
-          password = "supersecret";
-          encoding = "utf8";
-          pool = 5;
-        };
-        description = ''
-          Rails database configuration for Frab as Nix attribute set.
-        '';
-      };
-
-      extraEnvironment = mkOption {
-        type = types.attrs;
-        default = {};
-        example = {
-          FRAB_CURRENCY_UNIT = "€";
-          FRAB_CURRENCY_FORMAT = "%n%u";
-          EXCEPTION_EMAIL = "frab-owner@example.com";
-          SMTP_ADDRESS = "localhost";
-          SMTP_PORT = "587";
-          SMTP_DOMAIN = "localdomain";
-          SMTP_USER_NAME = "root";
-          SMTP_PASSWORD = "toor";
-          SMTP_AUTHENTICATION = "1";
-          SMTP_NOTLS = "1";
-        };
-        description = ''
-          Additional environment variables to set for frab for further
-          configuration. See the frab documentation for more information.
-        '';
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ frab-rake ];
-
-    users.users.${cfg.user} =
-      { group = cfg.group;
-        home = "${cfg.statePath}";
-        isSystemUser = true;
-      };
-
-    users.groups.${cfg.group} = { };
-
-    systemd.tmpfiles.rules = [
-      "d '${cfg.statePath}/system/attachments' - ${cfg.user} ${cfg.group} - -"
-    ];
-
-    systemd.services.frab = {
-      after = [ "network.target" "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
-      environment = frabEnv;
-
-      preStart = ''
-        ln -sf ${pkgs.writeText "frab-database.yml" databaseConfig} /run/frab/database.yml
-        ln -sf ${cfg.statePath}/system /run/frab/system
-
-        if ! test -e "${cfg.statePath}/db-setup-done"; then
-          ${frab-rake}/bin/frab-rake db:setup
-          touch ${cfg.statePath}/db-setup-done
-        else
-          ${frab-rake}/bin/frab-rake db:migrate
-        fi
-      '';
-
-      serviceConfig = {
-        PrivateTmp = true;
-        PrivateDevices = true;
-        Type = "simple";
-        User = cfg.user;
-        Group = cfg.group;
-        TimeoutSec = "300s";
-        Restart = "on-failure";
-        RestartSec = "10s";
-        RuntimeDirectory = "frab";
-        WorkingDirectory = "${package}/share/frab";
-        ExecStart = "${frab-rake}/bin/frab-bundle exec rails server " +
-          "--binding=${cfg.listenAddress} --port=${toString cfg.listenPort}";
-      };
-    };
-
-  };
-}
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
new file mode 100644
index 0000000000000..769490e915ac8
--- /dev/null
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.galene;
+  defaultstateDir = "/var/lib/galene";
+  defaultrecordingsDir = "${cfg.stateDir}/recordings";
+  defaultgroupsDir = "${cfg.stateDir}/groups";
+  defaultdataDir = "${cfg.stateDir}/data";
+in
+{
+  options = {
+    services.galene = {
+      enable = mkEnableOption "Galene Service.";
+
+      stateDir = mkOption {
+        default = defaultstateDir;
+        type = types.str;
+        description = ''
+          The directory where Galene stores its internal state. If left as the default
+          value this directory will automatically be created before the Galene server
+          starts, otherwise the sysadmin is responsible for ensuring the directory
+          exists with appropriate ownership and permissions.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "User account under which galene runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "Group under which galene runs.";
+      };
+
+      insecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether Galene should listen in http or in https. If left as the default
+          value (false), Galene needs to be fed a private key and a certificate.
+        '';
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/cert.pem";
+        description = ''
+          Path to the server's certificate. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      keyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/key.pem";
+        description = ''
+          Path to the server's private key. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "";
+        description = "HTTP listen address for galene.";
+      };
+
+      httpPort = mkOption {
+        type = types.port;
+        default = 8443;
+        description = "HTTP listen port.";
+      };
+
+      staticDir = mkOption {
+        type = types.str;
+        default = "${cfg.package.static}/static";
+        example = "/var/lib/galene/static";
+        description = "Web server directory.";
+      };
+
+      recordingsDir = mkOption {
+        type = types.str;
+        default = defaultrecordingsDir;
+        example = "/var/lib/galene/recordings";
+        description = "Recordings directory.";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = defaultdataDir;
+        example = "/var/lib/galene/data";
+        description = "Data directory.";
+      };
+
+      groupsDir = mkOption {
+        type = types.str;
+        default = defaultgroupsDir;
+        example = "/var/lib/galene/groups";
+        description = "Web server directory.";
+      };
+
+      package = mkOption {
+        default = pkgs.galene;
+        defaultText = "pkgs.galene";
+        type = types.package;
+        description = ''
+          Package for running Galene.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.insecure || (cfg.certFile != null && cfg.keyFile != null);
+        message = ''
+          Galene needs both certFile and keyFile defined for encryption, or
+          the insecure flag.
+        '';
+      }
+    ];
+
+    systemd.services.galene = {
+      description = "galene";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.certFile} ${cfg.dataDir}/cert.pem
+        install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.keyFile} ${cfg.dataDir}/key.pem
+      '';
+
+      serviceConfig = mkMerge [
+        {
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.stateDir;
+          ExecStart = ''${cfg.package}/bin/galene \
+          ${optionalString (cfg.insecure) "-insecure"} \
+          -data ${cfg.dataDir} \
+          -groups ${cfg.groupsDir} \
+          -recordings ${cfg.recordingsDir} \
+          -static ${cfg.staticDir}'';
+          Restart = "always";
+          # Upstream Requirements
+          LimitNOFILE = 65536;
+          StateDirectory = [ ] ++
+            optional (cfg.stateDir == defaultstateDir) "galene" ++
+            optional (cfg.dataDir == defaultdataDir) "galene/data" ++
+            optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
+            optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
+        }
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "galene")
+      {
+        galene = {
+          description = "galene Service";
+          group = cfg.group;
+          isSystemUser = true;
+        };
+      };
+
+    users.groups = mkIf (cfg.group == "galene") {
+      galene = { };
+    };
+  };
+  meta.maintainers = with lib.maintainers; [ rgrunbla ];
+}
diff --git a/nixos/modules/services/web-apps/gerrit.nix b/nixos/modules/services/web-apps/gerrit.nix
index 657b1a4fc5bac..864587aea5651 100644
--- a/nixos/modules/services/web-apps/gerrit.nix
+++ b/nixos/modules/services/web-apps/gerrit.nix
@@ -143,7 +143,7 @@ in
           Set a UUID that uniquely identifies the server.
 
           This can be generated with
-          <literal>nix-shell -p utillinux --run uuidgen</literal>.
+          <literal>nix-shell -p util-linux --run uuidgen</literal>.
         '';
       };
     };
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
index 568bdfd0c4297..be2de638dd961 100644
--- a/nixos/modules/services/web-apps/grocy.nix
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -115,9 +115,9 @@ in {
       user = "grocy";
       group = "nginx";
 
-      # PHP 7.3 is the only version which is supported/tested by upstream:
-      # https://github.com/grocy/grocy/blob/v2.6.0/README.md#how-to-install
-      phpPackage = pkgs.php73;
+      # PHP 7.4 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v3.0.0/README.md#how-to-install
+      phpPackage = pkgs.php74;
 
       inherit (cfg.phpfpm) settings;
 
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index c787c36b877c8..d940f3d3daecd 100644
--- a/nixos/modules/services/web-apps/codimd.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -3,31 +3,45 @@
 with lib;
 
 let
-  cfg = config.services.codimd;
+  cfg = config.services.hedgedoc;
+
+  # 21.03 will not be an official release - it was instead 21.05.  This
+  # versionAtLeast statement remains set to 21.03 for backwards compatibility.
+  # See https://github.com/NixOS/nixpkgs/pull/108899 and
+  # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
+  name = if versionAtLeast config.system.stateVersion "21.03"
+    then "hedgedoc"
+    else "codimd";
 
   prettyJSON = conf:
-    pkgs.runCommand "codimd-config.json" { preferLocalBuild = true; } ''
-      echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \
+    pkgs.runCommandLocal "hedgedoc-config.json" {
+      nativeBuildInputs = [ pkgs.jq ];
+    } ''
+      echo '${builtins.toJSON conf}' | jq \
         '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
     '';
 in
 {
-  options.services.codimd = {
-    enable = mkEnableOption "the CodiMD Markdown Editor";
+  imports = [
+    (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+  ];
+
+  options.services.hedgedoc = {
+    enable = mkEnableOption "the HedgeDoc Markdown Editor";
 
     groups = mkOption {
       type = types.listOf types.str;
       default = [];
       description = ''
-        Groups to which the codimd user should be added.
+        Groups to which the user ${name} should be added.
       '';
     };
 
     workDir = mkOption {
       type = types.path;
-      default = "/var/lib/codimd";
+      default = "/var/lib/${name}";
       description = ''
-        Working directory for the CodiMD service.
+        Working directory for the HedgeDoc service.
       '';
     };
 
@@ -36,17 +50,17 @@ in
       domain = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "codimd.org";
+        example = "hedgedoc.org";
         description = ''
-          Domain name for the CodiMD instance.
+          Domain name for the HedgeDoc instance.
         '';
       };
       urlPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/url/path/to/codimd";
+        example = "/url/path/to/hedgedoc";
         description = ''
-          Path under which CodiMD is accessible.
+          Path under which HedgeDoc is accessible.
         '';
       };
       host = mkOption {
@@ -67,7 +81,7 @@ in
       path = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/run/codimd.sock";
+        example = "/run/hedgedoc.sock";
         description = ''
           Specify where a UNIX domain socket should be placed.
         '';
@@ -75,7 +89,7 @@ in
       allowOrigin = mkOption {
         type = types.listOf types.str;
         default = [];
-        example = [ "localhost" "codimd.org" ];
+        example = [ "localhost" "hedgedoc.org" ];
         description = ''
           List of domains to whitelist.
         '';
@@ -199,7 +213,7 @@ in
         '';
         description = ''
           Specify which database to use.
-          CodiMD supports mysql, postgres, sqlite and mssql.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
           See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
           https://sequelize.readthedocs.io/en/v3/</link> for more information.
           Note: This option overrides <option>db</option>.
@@ -211,12 +225,12 @@ in
         example = literalExample ''
           {
             dialect = "sqlite";
-            storage = "/var/lib/codimd/db.codimd.sqlite";
+            storage = "/var/lib/${name}/db.${name}.sqlite";
           }
         '';
         description = ''
           Specify the configuration for sequelize.
-          CodiMD supports mysql, postgres, sqlite and mssql.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
           See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
           https://sequelize.readthedocs.io/en/v3/</link> for more information.
           Note: This option overrides <option>db</option>.
@@ -225,7 +239,7 @@ in
       sslKeyPath= mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/codimd.key";
+        example = "/var/lib/hedgedoc/hedgedoc.key";
         description = ''
           Path to the SSL key. Needed when <option>useSSL</option> is enabled.
         '';
@@ -233,7 +247,7 @@ in
       sslCertPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/codimd.crt";
+        example = "/var/lib/hedgedoc/hedgedoc.crt";
         description = ''
           Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
         '';
@@ -241,7 +255,7 @@ in
       sslCAPath = mkOption {
         type = types.listOf types.str;
         default = [];
-        example = [ "/var/lib/codimd/ca.crt" ];
+        example = [ "/var/lib/hedgedoc/ca.crt" ];
         description = ''
           SSL ca chain. Needed when <option>useSSL</option> is enabled.
         '';
@@ -249,7 +263,7 @@ in
       dhParamPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/dhparam.pem";
+        example = "/var/lib/hedgedoc/dhparam.pem";
         description = ''
           Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
         '';
@@ -258,10 +272,10 @@ in
         type = types.str;
         default = "/tmp";
         description = ''
-          Path to the temp directory CodiMD should use.
+          Path to the temp directory HedgeDoc should use.
           Note that <option>serviceConfig.PrivateTmp</option> is enabled for
-          the CodiMD systemd service by default.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          the HedgeDoc systemd service by default.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       defaultNotePath = mkOption {
@@ -269,7 +283,7 @@ in
         default = "./public/default.md";
         description = ''
           Path to the default Note file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       docsPath = mkOption {
@@ -277,7 +291,7 @@ in
         default = "./public/docs";
         description = ''
           Path to the docs directory.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       indexPath = mkOption {
@@ -285,7 +299,7 @@ in
         default = "./public/views/index.ejs";
         description = ''
           Path to the index template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       hackmdPath = mkOption {
@@ -293,7 +307,7 @@ in
         default = "./public/views/hackmd.ejs";
         description = ''
           Path to the hackmd template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       errorPath = mkOption {
@@ -302,7 +316,7 @@ in
         defaultText = "./public/views/error.ejs";
         description = ''
           Path to the error template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       prettyPath = mkOption {
@@ -311,7 +325,7 @@ in
         defaultText = "./public/views/pretty.ejs";
         description = ''
           Path to the pretty template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       slidePath = mkOption {
@@ -320,13 +334,13 @@ in
         defaultText = "./public/views/slide.hbs";
         description = ''
           Path to the slide template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       uploadsPath = mkOption {
         type = types.str;
         default = "${cfg.workDir}/uploads";
-        defaultText = "/var/lib/codimd/uploads";
+        defaultText = "/var/lib/${name}/uploads";
         description = ''
           Path under which uploaded files are saved.
         '';
@@ -764,7 +778,7 @@ in
               type = types.str;
               default = "";
               description = ''
-                LDAP field which is used as the username on CodiMD.
+                LDAP field which is used as the username on HedgeDoc.
                 By default <option>useridField</option> is used.
               '';
             };
@@ -772,7 +786,7 @@ in
               type = types.str;
               example = "uid";
               description = ''
-                LDAP field which is a unique identifier for users on CodiMD.
+                LDAP field which is a unique identifier for users on HedgeDoc.
               '';
             };
             tlsca = mkOption {
@@ -838,7 +852,7 @@ in
             requiredGroups = mkOption {
               type = types.listOf types.str;
               default = [];
-              example = [ "Hackmd-users" "Codimd-users" ];
+              example = [ "Hedgedoc-Users" ];
               description = ''
                 Required group names.
               '';
@@ -878,11 +892,10 @@ in
       };
     };
 
-
     environmentFile = mkOption {
       type = with types; nullOr path;
       default = null;
-      example = "/var/lib/codimd/codimd.env";
+      example = "/var/lib/hedgedoc/hedgedoc.env";
       description = ''
         Environment file as defined in <citerefentry>
         <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
@@ -893,9 +906,9 @@ in
         setting these variables accordingly in the environment file.
 
         <programlisting>
-          # snippet of CodiMD-related config
-          services.codimd.configuration.dbURL = "postgres://codimd:\''${DB_PASSWORD}@db-host:5432/codimddb";
-          services.codimd.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
+          # snippet of HedgeDoc-related config
+          services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
+          services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
         </programlisting>
 
         <programlisting>
@@ -905,7 +918,15 @@ in
         </programlisting>
 
         Note that this file needs to be available on the host on which
-        <literal>CodiMD</literal> is running.
+        <literal>HedgeDoc</literal> is running.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.hedgedoc;
+      description = ''
+        Package that provides HedgeDoc.
       '';
     };
   };
@@ -915,20 +936,20 @@ in
       { assertion = cfg.configuration.db == {} -> (
           cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
         );
-        message = "Database configuration for CodiMD missing."; }
+        message = "Database configuration for HedgeDoc missing."; }
     ];
-    users.groups.codimd = {};
-    users.users.codimd = {
-      description = "CodiMD service user";
-      group = "codimd";
+    users.groups.${name} = {};
+    users.users.${name} = {
+      description = "HedgeDoc service user";
+      group = name;
       extraGroups = cfg.groups;
       home = cfg.workDir;
       createHome = true;
       isSystemUser = true;
     };
 
-    systemd.services.codimd = {
-      description = "CodiMD Service";
+    systemd.services.hedgedoc = {
+      description = "HedgeDoc Service";
       wantedBy = [ "multi-user.target" ];
       after = [ "networking.target" ];
       preStart = ''
@@ -938,14 +959,14 @@ in
       '';
       serviceConfig = {
         WorkingDirectory = cfg.workDir;
-        ExecStart = "${pkgs.codimd}/bin/codimd";
+        ExecStart = "${cfg.package}/bin/hedgedoc";
         EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
         Environment = [
           "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
           "NODE_ENV=production"
         ];
         Restart = "always";
-        User = "codimd";
+        User = name;
         PrivateTmp = true;
       };
     };
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
new file mode 100644
index 0000000000000..43fc4daa177fe
--- /dev/null
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -0,0 +1,77 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.hledger-web;
+in {
+  options.services.hledger-web = {
+
+    enable = mkEnableOption "hledger-web service";
+
+    serveApi = mkEnableOption "Serve only the JSON web API, without the web UI.";
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to listen on.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      example = "80";
+      description = ''
+        Port to listen on.
+      '';
+    };
+
+    capabilities = mkOption {
+      type = types.commas;
+      default = "view";
+      description = ''
+        Enable the view, add, and/or manage capabilities. E.g. view,add
+      '';
+    };
+
+    journalFile = mkOption {
+      type = types.path;
+      example = "/home/hledger/.hledger.journal";
+      description = ''
+        Input journal file.
+      '';
+    };
+
+    baseUrl = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "https://example.org";
+      description = ''
+        Base URL, when sharing over a network.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.hledger-web = {
+      description = "hledger-web - web-app for the hledger accounting tool.";
+      documentation = [ https://hledger.org/hledger-web.html ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.hledger-web}/bin/hledger-web \
+          --host=${cfg.host} \
+          --port=${toString cfg.port} \
+          --file=${cfg.journalFile}  \
+          "--capabilities=${cfg.capabilities}" \
+          ${optionalString (cfg.baseUrl != null) "--base-url=${cfg.baseUrl}"} \
+          ${optionalString (cfg.serveApi) "--serve-api"}
+        '';
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ marijanp ];
+}
diff --git a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
index d9ad7e9e3d397..eea49bda283bf 100644
--- a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
+++ b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -167,8 +167,8 @@ in {
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         user = "icingaweb2";
+        phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
         phpOptions = ''
-          extension = ${pkgs.phpPackages.imagick}/lib/php/extensions/imagick.so
           date.timezone = "${cfg.timezone}"
         '';
         settings = mapAttrs (name: mkDefault) {
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
index 68769ac8c0316..b4987fa4702cf 100644
--- a/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -44,7 +44,7 @@ let
 in
   {
     options.services.ihatemoney = {
-      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
+      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode";
       backend = mkOption {
         type = types.enum [ "sqlite" "postgresql" ];
         default = "sqlite";
@@ -116,16 +116,13 @@ in
       services.uwsgi = {
         enable = true;
         plugins = [ "python3" ];
-        # the vassal needs to be able to setuid
-        user = "root";
-        group = "root";
         instance = {
           type = "emperor";
           vassals.ihatemoney = {
             type = "normal";
             strict = true;
-            uid = user;
-            gid = group;
+            immediate-uid = user;
+            immediate-gid = group;
             # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
             enable-threads = true;
             module = "wsgi:application";
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
new file mode 100644
index 0000000000000..a93e93279331e
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -0,0 +1,692 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.keycloak;
+in
+{
+  options.services.keycloak = {
+
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the Keycloak identity and access management
+        server.
+      '';
+    };
+
+    bindAddress = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.bind.address:0.0.0.0}";
+      example = "127.0.0.1";
+      description = ''
+        On which address Keycloak should accept new connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    httpPort = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.http.port:80}";
+      example = "8080";
+      description = ''
+        On which port Keycloak should listen for new HTTP connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    httpsPort = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.https.port:443}";
+      example = "8443";
+      description = ''
+        On which port Keycloak should listen for new HTTPS connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    frontendUrl = lib.mkOption {
+      type = lib.types.str;
+      example = "keycloak.example.com/auth";
+      description = ''
+        The public URL used as base for all frontend requests. Should
+        normally include a trailing <literal>/auth</literal>.
+
+        See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+        Hostname section of the Keycloak server installation
+        manual</link> for more information.
+      '';
+    };
+
+    forceBackendUrlToFrontendUrl = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether Keycloak should force all requests to go through the
+        frontend URL configured in <xref
+        linkend="opt-services.keycloak.frontendUrl" />. By default,
+        Keycloak allows backend requests to instead use its local
+        hostname or IP address and may also advertise it to clients
+        through its OpenID Connect Discovery endpoint.
+
+        See <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+        Hostname section of the Keycloak server installation
+        manual</link> for more information.
+      '';
+    };
+
+    certificatePrivateKeyBundle = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/keys/ssl_cert";
+      description = ''
+        The path to a PEM formatted bundle of the private key and
+        certificate to use for TLS connections.
+
+        This should be a string, not a Nix path, since Nix paths are
+        copied into the world-readable Nix store.
+      '';
+    };
+
+    databaseType = lib.mkOption {
+      type = lib.types.enum [ "mysql" "postgresql" ];
+      default = "postgresql";
+      example = "mysql";
+      description = ''
+        The type of database Keycloak should connect to.
+      '';
+    };
+
+    databaseHost = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = ''
+        Hostname of the database to connect to.
+      '';
+    };
+
+    databasePort =
+      let
+        dbPorts = {
+          postgresql = 5432;
+          mysql = 3306;
+        };
+      in
+        lib.mkOption {
+          type = lib.types.port;
+          default = dbPorts.${cfg.databaseType};
+          description = ''
+            Port of the database to connect to.
+          '';
+        };
+
+    databaseUseSSL = lib.mkOption {
+      type = lib.types.bool;
+      default = cfg.databaseHost != "localhost";
+      description = ''
+        Whether the database connection should be secured by SSL /
+        TLS.
+      '';
+    };
+
+    databaseCaCert = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = ''
+        The SSL / TLS CA certificate that verifies the identity of the
+        database server.
+
+        Required when PostgreSQL is used and SSL is turned on.
+
+        For MySQL, if left at <literal>null</literal>, the default
+        Java keystore is used, which should suffice if the server
+        certificate is issued by an official CA.
+      '';
+    };
+
+    databaseCreateLocally = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = ''
+        Whether a database should be automatically created on the
+        local host. Set this to false if you plan on provisioning a
+        local database yourself. This has no effect if
+        services.keycloak.databaseHost is customized.
+      '';
+    };
+
+    databaseUsername = lib.mkOption {
+      type = lib.types.str;
+      default = "keycloak";
+      description = ''
+        Username to use when connecting to an external or manually
+        provisioned database; has no effect when a local database is
+        automatically provisioned.
+      '';
+    };
+
+    databasePasswordFile = lib.mkOption {
+      type = lib.types.path;
+      example = "/run/keys/db_password";
+      description = ''
+        File containing the database password.
+
+        This should be a string, not a Nix path, since Nix paths are
+        copied into the world-readable Nix store.
+      '';
+    };
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.keycloak;
+      description = ''
+        Keycloak package to use.
+      '';
+    };
+
+    initialAdminPassword = lib.mkOption {
+      type = lib.types.str;
+      default = "changeme";
+      description = ''
+        Initial password set for the <literal>admin</literal>
+        user. The password is not stored safely and should be changed
+        immediately in the admin panel.
+      '';
+    };
+
+    extraConfig = lib.mkOption {
+      type = lib.types.attrs;
+      default = { };
+      example = lib.literalExample ''
+        {
+          "subsystem=keycloak-server" = {
+            "spi=hostname" = {
+              "provider=default" = null;
+              "provider=fixed" = {
+                enabled = true;
+                properties.hostname = "keycloak.example.com";
+              };
+              default-provider = "fixed";
+            };
+          };
+        }
+      '';
+      description = ''
+        Additional Keycloak configuration options to set in
+        <literal>standalone.xml</literal>.
+
+        Options are expressed as a Nix attribute set which matches the
+        structure of the jboss-cli configuration. The configuration is
+        effectively overlayed on top of the default configuration
+        shipped with Keycloak. To remove existing nodes and undefine
+        attributes from the default configuration, set them to
+        <literal>null</literal>.
+
+        The example configuration does the equivalent of the following
+        script, which removes the hostname provider
+        <literal>default</literal>, adds the deprecated hostname
+        provider <literal>fixed</literal> and defines it the default:
+
+        <programlisting>
+        /subsystem=keycloak-server/spi=hostname/provider=default:remove()
+        /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+        /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+        </programlisting>
+
+        You can discover available options by using the <link
+        xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+        program and by referring to the <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+        Server Installation and Configuration Guide</link>.
+      '';
+    };
+
+  };
+
+  config =
+    let
+      # We only want to create a database if we're actually going to connect to it.
+      databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "localhost";
+      createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.databaseType == "postgresql";
+      createLocalMySQL = databaseActuallyCreateLocally && cfg.databaseType == "mysql";
+
+      mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} ''
+        ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt
+      '';
+
+      keycloakConfig' = builtins.foldl' lib.recursiveUpdate {
+        "interface=public".inet-address = cfg.bindAddress;
+        "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
+        "subsystem=keycloak-server"."spi=hostname" = {
+          "provider=default" = {
+            enabled = true;
+            properties = {
+              inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
+            };
+          };
+        };
+        "subsystem=datasources"."data-source=KeycloakDS" = {
+          max-pool-size = "20";
+          user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername;
+          password = "@db-password@";
+        };
+      } [
+        (lib.optionalAttrs (cfg.databaseType == "postgresql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=postgresql" = {
+              driver-module-name = "org.postgresql";
+              driver-name = "postgresql";
+              driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak";
+              driver-name = "postgresql";
+              "connection-properties=ssl".value = lib.boolToString cfg.databaseUseSSL;
+            } // (lib.optionalAttrs (cfg.databaseCaCert != null) {
+              "connection-properties=sslrootcert".value = cfg.databaseCaCert;
+              "connection-properties=sslmode".value = "verify-ca";
+            });
+          };
+        })
+        (lib.optionalAttrs (cfg.databaseType == "mysql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=mysql" = {
+              driver-module-name = "com.mysql";
+              driver-name = "mysql";
+              driver-class-name = "com.mysql.jdbc.Driver";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak";
+              driver-name = "mysql";
+              "connection-properties=useSSL".value = lib.boolToString cfg.databaseUseSSL;
+              "connection-properties=requireSSL".value = lib.boolToString cfg.databaseUseSSL;
+              "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.databaseUseSSL;
+              "connection-properties=characterEncoding".value = "UTF-8";
+              valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
+              validate-on-match = true;
+              exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
+            } // (lib.optionalAttrs (cfg.databaseCaCert != null) {
+              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
+              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
+            });
+          };
+        })
+        (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) {
+          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
+          "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = {
+            keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
+            keystore-password = "notsosecretpassword";
+          };
+          "subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm";
+        })
+        cfg.extraConfig
+      ];
+
+
+      /* Produces a JBoss CLI script that creates paths and sets
+         attributes matching those described by `attrs`. When the
+         script is run, the existing settings are effectively overlayed
+         by those from `attrs`. Existing attributes can be unset by
+         defining them `null`.
+
+         JBoss paths and attributes / maps are distinguished by their
+         name, where paths follow a `key=value` scheme.
+
+         Example:
+           mkJbossScript {
+             "subsystem=keycloak-server"."spi=hostname" = {
+               "provider=fixed" = null;
+               "provider=default" = {
+                 enabled = true;
+                 properties = {
+                   inherit frontendUrl;
+                   forceBackendUrlToFrontendUrl = false;
+                 };
+               };
+             };
+           }
+           => ''
+             if (outcome != success) of /:read-resource()
+                 /:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server:read-resource()
+                 /subsystem=keycloak-server:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
+                 /subsystem=keycloak-server/spi=hostname:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
+             end-if
+             if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+             end-if
+             if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+             end-if
+             if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
+             end-if
+           ''
+      */
+      mkJbossScript = attrs:
+        let
+          /* From a JBoss path and an attrset, produces a JBoss CLI
+             snippet that writes the corresponding attributes starting
+             at `path`. Recurses down into subattrsets as necessary,
+             producing the variable name from its full path in the
+             attrset.
+
+             Example:
+               writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+                 end-if
+                 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+                 end-if
+                 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+                 end-if
+               ''
+          */
+          writeAttributes = path: set:
+            let
+              # JBoss expressions like `${var}` need to be prefixed
+              # with `expression` to evaluate.
+              prefixExpression = string:
+                let
+                  match = (builtins.match ''"\$\{.*}"'' string);
+                in
+                  if match != null then
+                    "expression " + string
+                  else
+                    string;
+
+              writeAttribute = attribute: value:
+                let
+                  type = builtins.typeOf value;
+                in
+                  if type == "set" then
+                    let
+                      names = builtins.attrNames value;
+                    in
+                      builtins.foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
+                  else if value == null then ''
+                    if (outcome == success) of ${path}:read-attribute(name="${attribute}")
+                        ${path}:undefine-attribute(name="${attribute}")
+                    end-if
+                  ''
+                  else if builtins.elem type [ "string" "path" "bool" ] then
+                    let
+                      value' = if type == "bool" then lib.boolToString value else ''"${value}"'';
+                    in ''
+                      if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
+                        ${path}:write-attribute(name=${attribute}, value=${value'})
+                      end-if
+                    ''
+                  else throw "Unsupported type '${type}' for path '${path}'!";
+            in
+              lib.concatStrings
+                (lib.mapAttrsToList
+                  (attribute: value: (writeAttribute attribute value))
+                  set);
+
+
+          /* Produces an argument list for the JBoss `add()` function,
+             which adds a JBoss path and takes as its arguments the
+             required subpaths and attributes.
+
+             Example:
+               makeArgList {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
+               ''
+          */
+          makeArgList = set:
+            let
+              makeArg = attribute: value:
+                let
+                  type = builtins.typeOf value;
+                in
+                  if type == "set" then
+                    "${attribute} = { " + (makeArgList value) + " }"
+                  else if builtins.elem type [ "string" "path" "bool" ] then
+                    "${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}"
+                  else if value == null then
+                    ""
+                  else
+                    throw "Unsupported type '${type}' for attribute '${attribute}'!";
+            in
+              lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set);
+
+
+          /* Recurses into the `attrs` attrset, beginning at the path
+             resolved from `state.path ++ node`; if `node` is `null`,
+             starts from `state.path`. Only subattrsets that are JBoss
+             paths, i.e. follows the `key=value` format, are recursed
+             into - the rest are considered JBoss attributes / maps.
+          */
+          recurse = state: node:
+            let
+              path = state.path ++ (lib.optional (node != null) node);
+              isPath = name:
+                let
+                  value = lib.getAttrFromPath (path ++ [ name ]) attrs;
+                in
+                  if (builtins.match ".*([=]).*" name) == [ "=" ] then
+                    if builtins.isAttrs value || value == null then
+                      true
+                    else
+                      throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
+                  else
+                    false;
+              jbossPath = "/" + (lib.concatStringsSep "/" path);
+              nodeValue = lib.getAttrFromPath path attrs;
+              children = if !builtins.isAttrs nodeValue then {} else nodeValue;
+              subPaths = builtins.filter isPath (builtins.attrNames children);
+              jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children;
+            in
+              state // {
+                text = state.text + (
+                  if nodeValue != null then ''
+                    if (outcome != success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:add(${makeArgList jbossAttrs})
+                    end-if
+                  '' + (writeAttributes jbossPath jbossAttrs)
+                  else ''
+                    if (outcome == success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:remove()
+                    end-if
+                  '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text;
+              };
+        in
+          (recurse { text = ""; path = []; } null).text;
+
+
+      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
+
+      keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} ''
+        export JBOSS_BASE_DIR="$(pwd -P)";
+        export JBOSS_MODULEPATH="${cfg.package}/modules";
+        export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log";
+
+        cp -r ${cfg.package}/standalone/configuration .
+        chmod -R u+rwX ./configuration
+
+        mkdir -p {deployments,ssl}
+
+        "${cfg.package}/bin/standalone.sh"&
+
+        attempt=1
+        max_attempts=30
+        while ! ${cfg.package}/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'; do
+            if [[ "$attempt" == "$max_attempts" ]]; then
+                echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2
+                exit 1
+            fi
+            echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
+            sleep 1
+            (( attempt++ ))
+        done
+
+        ${cfg.package}/bin/jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+
+        cp configuration/standalone.xml $out
+      '';
+    in
+      lib.mkIf cfg.enable {
+
+        assertions = [
+          {
+            assertion = (cfg.databaseUseSSL && cfg.databaseType == "postgresql") -> (cfg.databaseCaCert != null);
+            message = "A CA certificate must be specified (in 'services.keycloak.databaseCaCert') when PostgreSQL is used with SSL";
+          }
+        ];
+
+        environment.systemPackages = [ cfg.package ];
+
+        systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL {
+          after = [ "postgresql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "postgresql.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = "postgres";
+            Group = "postgres";
+          };
+          script = ''
+            set -eu
+
+            PSQL=${config.services.postgresql.package}/bin/psql
+
+            db_password="$(<'${cfg.databasePasswordFile}')"
+            $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tAc "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB"
+            $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
+          '';
+        };
+
+        systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL {
+          after = [ "mysql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "mysql.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = config.services.mysql.user;
+            Group = config.services.mysql.group;
+          };
+          script = ''
+            set -eu
+
+            db_password="$(<'${cfg.databasePasswordFile}')"
+            ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
+              echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
+              echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
+            ) | ${config.services.mysql.package}/bin/mysql -N
+          '';
+        };
+
+        systemd.services.keycloak =
+          let
+            databaseServices =
+              if createLocalPostgreSQL then [
+                "keycloakPostgreSQLInit.service" "postgresql.service"
+              ]
+              else if createLocalMySQL then [
+                "keycloakMySQLInit.service" "mysql.service"
+              ]
+              else [ ];
+          in {
+            after = databaseServices;
+            bindsTo = databaseServices;
+            wantedBy = [ "multi-user.target" ];
+            environment = {
+              JBOSS_LOG_DIR = "/var/log/keycloak";
+              JBOSS_BASE_DIR = "/run/keycloak";
+              JBOSS_MODULEPATH = "${cfg.package}/modules";
+            };
+            serviceConfig = {
+              ExecStartPre = let
+                startPreFullPrivileges = ''
+                  set -eu
+
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password
+                '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) ''
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle
+                '';
+                startPre = ''
+                  set -eu
+
+                  install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
+                  install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
+
+                  db_password="$(</run/keycloak/secrets/db_password)"
+                  ${pkgs.replace}/bin/replace-literal -fe '@db-password@' "$db_password" /run/keycloak/configuration/standalone.xml
+
+                  export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
+                  ${cfg.package}/bin/add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
+                '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) ''
+                  pushd /run/keycloak/ssl/
+                  cat /run/keycloak/secrets/ssl_cert_pk_bundle <(echo) /etc/ssl/certs/ca-certificates.crt > allcerts.pem
+                  ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain \
+                                                     -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
+                                                     -CAfile allcerts.pem -passout pass:notsosecretpassword
+                  popd
+                '';
+              in [
+                "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}"
+                "${pkgs.writeShellScript "keycloak-start-pre" startPre}"
+              ];
+              ExecStart = "${cfg.package}/bin/standalone.sh";
+              User = "keycloak";
+              Group = "keycloak";
+              DynamicUser = true;
+              RuntimeDirectory = map (p: "keycloak/" + p) [
+                "secrets"
+                "configuration"
+                "deployments"
+                "data"
+                "ssl"
+                "log"
+                "tmp"
+              ];
+              RuntimeDirectoryMode = 0700;
+              LogsDirectory = "keycloak";
+              AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+            };
+          };
+
+        services.postgresql.enable = lib.mkDefault createLocalPostgreSQL;
+        services.mysql.enable = lib.mkDefault createLocalMySQL;
+        services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql;
+      };
+
+  meta.doc = ./keycloak.xml;
+}
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
new file mode 100644
index 0000000000000..ca5e223eee467
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -0,0 +1,205 @@
+<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-keycloak">
+ <title>Keycloak</title>
+ <para>
+   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
+   open source identity and access management server with support for
+   <link xlink:href="https://openid.net/connect/">OpenID
+   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
+   2.0</link> and <link
+   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
+   2.0</link>.
+ </para>
+   <section xml:id="module-services-keycloak-admin">
+     <title>Administration</title>
+     <para>
+       An administrative user with the username
+       <literal>admin</literal> is automatically created in the
+       <literal>master</literal> realm. Its initial password can be
+       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
+       and defaults to <literal>changeme</literal>. The password is
+       not stored safely and should be changed immediately in the
+       admin panel.
+     </para>
+
+     <para>
+       Refer to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
+       Console section of the Keycloak Server Administration Guide</link> for
+       information on how to administer your
+       <productname>Keycloak</productname> instance.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-database">
+     <title>Database access</title>
+     <para>
+       <productname>Keycloak</productname> can be used with either
+       <productname>PostgreSQL</productname> or
+       <productname>MySQL</productname>. Which one is used can be
+       configured in <xref
+       linkend="opt-services.keycloak.databaseType" />. The selected
+       database will automatically be enabled and a database and role
+       created unless <xref
+       linkend="opt-services.keycloak.databaseHost" /> is changed from
+       its default of <literal>localhost</literal> or <xref
+       linkend="opt-services.keycloak.databaseCreateLocally" /> is set
+       to <literal>false</literal>.
+     </para>
+
+     <para>
+       External database access can also be configured by setting
+       <xref linkend="opt-services.keycloak.databaseHost" />, <xref
+       linkend="opt-services.keycloak.databaseUsername" />, <xref
+       linkend="opt-services.keycloak.databaseUseSSL" /> and <xref
+       linkend="opt-services.keycloak.databaseCaCert" /> as
+       appropriate. Note that you need to manually create a database
+       called <literal>keycloak</literal> and allow the configured
+       database user full access to it.
+     </para>
+
+     <para>
+       <xref linkend="opt-services.keycloak.databasePasswordFile" />
+       must be set to the path to a file containing the password used
+       to log in to the database. If <xref linkend="opt-services.keycloak.databaseHost" />
+       and <xref linkend="opt-services.keycloak.databaseCreateLocally" />
+       are kept at their defaults, the database role
+       <literal>keycloak</literal> with that password is provisioned
+       on the local database instance.
+     </para>
+
+     <warning>
+       <para>
+         The path should be provided as a string, not a Nix path, since Nix
+         paths are copied into the world readable Nix store.
+       </para>
+     </warning>
+   </section>
+
+   <section xml:id="module-services-keycloak-frontendurl">
+     <title>Frontend URL</title>
+     <para>
+       The frontend URL is used as base for all frontend requests and
+       must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
+       It should normally include a trailing <literal>/auth</literal>
+       (the default web context).
+     </para>
+
+     <para>
+       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       determines whether Keycloak should force all requests to go
+       through the frontend URL. By default,
+       <productname>Keycloak</productname> allows backend requests to
+       instead use its local hostname or IP address and may also
+       advertise it to clients through its OpenID Connect Discovery
+       endpoint.
+     </para>
+
+     <para>
+       See the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
+       section of the Keycloak Server Installation and Configuration
+       Guide</link> for more information.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-tls">
+     <title>Setting up TLS/SSL</title>
+     <para>
+       By default, <productname>Keycloak</productname> won't accept
+       unsecured HTTP connections originating from outside its local
+       network.
+     </para>
+
+     <para>
+       For HTTPS support, a TLS certificate and private key is
+       required. They should be <link
+       xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
+       formatted</link> and concatenated into a single file. The path
+       to this file should be configured in
+       <xref linkend="opt-services.keycloak.certificatePrivateKeyBundle" />.
+     </para>
+
+     <warning>
+       <para>
+         The path should be provided as a string, not a Nix path,
+         since Nix paths are copied into the world readable Nix store.
+       </para>
+     </warning>
+   </section>
+
+   <section xml:id="module-services-keycloak-extra-config">
+     <title>Additional configuration</title>
+     <para>
+       Additional Keycloak configuration options, for which no
+       explicit <productname>NixOS</productname> options are provided,
+       can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
+     </para>
+
+     <para>
+       Options are expressed as a Nix attribute set which matches the
+       structure of the jboss-cli configuration. The configuration is
+       effectively overlayed on top of the default configuration
+       shipped with Keycloak. To remove existing nodes and undefine
+       attributes from the default configuration, set them to
+       <literal>null</literal>.
+     </para>
+     <para>
+       For example, the following script, which removes the hostname
+       provider <literal>default</literal>, adds the deprecated
+       hostname provider <literal>fixed</literal> and defines it the
+       default:
+
+<programlisting>
+/subsystem=keycloak-server/spi=hostname/provider=default:remove()
+/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+</programlisting>
+
+       would be expressed as
+
+<programlisting>
+services.keycloak.extraConfig = {
+  "subsystem=keycloak-server" = {
+    "spi=hostname" = {
+      "provider=default" = null;
+      "provider=fixed" = {
+        enabled = true;
+        properties.hostname = "keycloak.example.com";
+      };
+      default-provider = "fixed";
+    };
+  };
+};
+</programlisting>
+     </para>
+     <para>
+       You can discover available options by using the <link
+       xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+       program and by referring to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+       Server Installation and Configuration Guide</link>.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-example-config">
+     <title>Example configuration</title>
+     <para>
+       A basic configuration with some custom settings could look like this:
+<programlisting>
+services.keycloak = {
+  <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
+  <link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
+  <link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
+  <link linkend="opt-services.keycloak.certificatePrivateKeyBundle">certificatePrivateKeyBundle</link> = "/run/keys/ssl_cert";
+  <link linkend="opt-services.keycloak.databasePasswordFile">databasePasswordFile</link> = "/run/keys/db_password";
+};
+</programlisting>
+     </para>
+
+   </section>
+ </chapter>
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
new file mode 100644
index 0000000000000..ea7aebc3b12d5
--- /dev/null
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -0,0 +1,564 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.mastodon;
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
+
+  env = {
+    RAILS_ENV = "production";
+    NODE_ENV = "production";
+
+    DB_USER = cfg.database.user;
+
+    REDIS_HOST = cfg.redis.host;
+    REDIS_PORT = toString(cfg.redis.port);
+    DB_HOST = cfg.database.host;
+    DB_PORT = toString(cfg.database.port);
+    DB_NAME = cfg.database.name;
+    LOCAL_DOMAIN = cfg.localDomain;
+    SMTP_SERVER = cfg.smtp.host;
+    SMTP_PORT = toString(cfg.smtp.port);
+    SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
+    PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
+    PAPERCLIP_ROOT_URL = "/system";
+    ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
+    ES_HOST = cfg.elasticsearch.host;
+    ES_PORT = toString(cfg.elasticsearch.port);
+
+    TRUSTED_PROXY_IP = cfg.trustedProxy;
+  }
+  // (if cfg.smtp.authenticate then { SMTP_LOGIN  = cfg.smtp.user; } else {})
+  // cfg.extraConfig;
+
+  cfgService = {
+    # User and group
+    User = cfg.user;
+    Group = cfg.group;
+    # State directory and mode
+    StateDirectory = "mastodon";
+    StateDirectoryMode = "0750";
+    # Logs directory and mode
+    LogsDirectory = "mastodon";
+    LogsDirectoryMode = "0750";
+    # Access write directories
+    UMask = "0027";
+    # Sandboxing
+    PrivateTmp = true;
+  };
+
+  envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
+    set -a
+    source "${envFile}"
+    source /var/lib/mastodon/.secrets_env
+    eval -- "\$@"
+  '';
+
+in {
+
+  options = {
+    services.mastodon = {
+      enable = lib.mkEnableOption "Mastodon, a federated social network server";
+
+      configureNginx = lib.mkOption {
+        description = ''
+          Configure nginx as a reverse proxy for mastodon.
+          Note that this makes some assumptions on your setup, and sets settings that will
+          affect other virtualHosts running on your nginx instance, if any.
+          Alternatively you can configure a reverse-proxy of your choice to serve these paths:
+
+          <code>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</code>
+
+          <code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.)
+
+          <code>/system/ -> /var/lib/mastodon/public-system/</code>
+
+          <code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code>
+
+          Make sure that websockets are forwarded properly. You might want to set up caching
+          of some requests. Take a look at mastodon's provided nginx configuration at
+          <code>https://github.com/tootsuite/mastodon/blob/master/dist/nginx.conf</code>.
+        '';
+        type = lib.types.bool;
+        default = false;
+      };
+
+      user = lib.mkOption {
+        description = ''
+          User under which mastodon runs. If it is set to "mastodon",
+          that user will be created, otherwise it should be set to the
+          name of a user created elsewhere.  In both cases,
+          <package>mastodon</package> and a package containing only
+          the shell script <code>mastodon-env</code> will be added to
+          the user's package set. To run a command from
+          <package>mastodon</package> such as <code>tootctl</code>
+          with the environment configured by this module use
+          <code>mastodon-env</code>, as in:
+
+          <code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code>
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      group = lib.mkOption {
+        description = ''
+          Group under which mastodon runs.
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      streamingPort = lib.mkOption {
+        description = "TCP port used by the mastodon-streaming service.";
+        type = lib.types.port;
+        default = 55000;
+      };
+
+      webPort = lib.mkOption {
+        description = "TCP port used by the mastodon-web service.";
+        type = lib.types.port;
+        default = 55001;
+      };
+
+      sidekiqPort = lib.mkOption {
+        description = "TCP port used by the mastodon-sidekiq service";
+        type = lib.types.port;
+        default = 55002;
+      };
+
+      vapidPublicKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the public key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If <option>mastodon.vapidPrivateKeyFile</option>does not
+          exist, it and this file will be created with a new keypair.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-public-key";
+        type = lib.types.str;
+      };
+
+      localDomain = lib.mkOption {
+        description = "The domain serving your Mastodon instance.";
+        example = "social.example.org";
+        type = lib.types.str;
+      };
+
+      secretKeyBaseFile = lib.mkOption {
+        description = ''
+          Path to file containing the secret key base.
+          A new secret key base can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new secret key base.
+        '';
+        default = "/var/lib/mastodon/secrets/secret-key-base";
+        type = lib.types.str;
+      };
+
+      otpSecretFile = lib.mkOption {
+        description = ''
+          Path to file containing the OTP secret.
+          A new OTP secret can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new OTP secret.
+        '';
+        default = "/var/lib/mastodon/secrets/otp-secret";
+        type = lib.types.str;
+      };
+
+      vapidPrivateKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the private key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If this file does not exist, it will be created with a new
+          private key.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-private-key";
+        type = lib.types.str;
+      };
+
+      trustedProxy = lib.mkOption {
+        description = ''
+          You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
+          otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
+          bad because IP addresses are used for important rate limits and security functions.
+        '';
+        type = lib.types.str;
+        default = "127.0.0.1";
+      };
+
+      enableUnixSocket = lib.mkOption {
+        description = ''
+          Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
+          is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
+          processes and streaming API (Node.js) processes.
+        '';
+        type = lib.types.bool;
+        default = true;
+      };
+
+      redis = {
+        createLocally = lib.mkOption {
+          description = "Configure local Redis server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          description = "Redis host.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "Redis port.";
+          type = lib.types.port;
+          default = 6379;
+        };
+      };
+
+      database = {
+        createLocally = lib.mkOption {
+          description = "Configure local PostgreSQL database server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          type = lib.types.str;
+          default = "/run/postgresql";
+          example = "192.168.23.42";
+          description = "Database host address or unix socket.";
+        };
+
+        port = lib.mkOption {
+          type = lib.types.int;
+          default = 5432;
+          description = "Database host port.";
+        };
+
+        name = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database name.";
+        };
+
+        user = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database user.";
+        };
+
+        passwordFile = lib.mkOption {
+          type = lib.types.nullOr lib.types.path;
+          default = "/var/lib/mastodon/secrets/db-password";
+          example = "/run/keys/mastodon-db-password";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+      };
+
+      smtp = {
+        createLocally = lib.mkOption {
+          description = "Configure local Postfix SMTP server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        authenticate = lib.mkOption {
+          description = "Authenticate with the SMTP server using username and password.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          description = "SMTP host used when sending emails to users.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "SMTP port used when sending emails to users.";
+          type = lib.types.port;
+          default = 25;
+        };
+
+        fromAddress = lib.mkOption {
+          description = ''"From" address used when sending Emails to users.'';
+          type = lib.types.str;
+        };
+
+        user = lib.mkOption {
+          description = "SMTP login name.";
+          type = lib.types.str;
+        };
+
+        passwordFile = lib.mkOption {
+          description = ''
+            Path to file containing the SMTP password.
+          '';
+          default = "/var/lib/mastodon/secrets/smtp-password";
+          example = "/run/keys/mastodon-smtp-password";
+          type = lib.types.str;
+        };
+      };
+
+      elasticsearch = {
+        host = lib.mkOption {
+          description = ''
+            Elasticsearch host.
+            If it is not null, Elasticsearch full text search will be enabled.
+          '';
+          type = lib.types.nullOr lib.types.str;
+          default = null;
+        };
+
+        port = lib.mkOption {
+          description = "Elasticsearch port.";
+          type = lib.types.port;
+          default = 9200;
+        };
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.mastodon;
+        defaultText = "pkgs.mastodon";
+        description = "Mastodon package to use.";
+      };
+
+      extraConfig = lib.mkOption {
+        type = lib.types.attrs;
+        default = {};
+        description = ''
+          Extra environment variables to pass to all mastodon services.
+        '';
+      };
+
+      automaticMigrations = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Do automatic database migrations.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
+        message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.'';
+      }
+    ];
+
+    systemd.services.mastodon-init-dirs = {
+      script = ''
+        umask 077
+
+        if ! test -f ${cfg.secretKeyBaseFile}; then
+          mkdir -p $(dirname ${cfg.secretKeyBaseFile})
+          bin/rake secret > ${cfg.secretKeyBaseFile}
+        fi
+        if ! test -f ${cfg.otpSecretFile}; then
+          mkdir -p $(dirname ${cfg.otpSecretFile})
+          bin/rake secret > ${cfg.otpSecretFile}
+        fi
+        if ! test -f ${cfg.vapidPrivateKeyFile}; then
+          mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
+          keypair=$(bin/rake webpush:generate_keys)
+          echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
+          echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
+        fi
+
+        cat > /var/lib/mastodon/.secrets_env <<EOF
+        SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
+        OTP_SECRET="$(cat ${cfg.otpSecretFile})"
+        VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
+        VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
+        DB_PASS="$(cat ${cfg.database.passwordFile})"
+      '' + (if cfg.smtp.authenticate then ''
+        SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
+      '' else "") + ''
+        EOF
+      '';
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        WorkingDirectory = cfg.package;
+      } // cfgService;
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
+      script = ''
+        if [ `psql ${cfg.database.name} -c \
+                "select count(*) from pg_class c \
+                join pg_namespace s on s.oid = c.relnamespace \
+                where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
+                and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
+          SAFETY_ASSURED=1 rake db:schema:load
+          rake db:seed
+        else
+          rake db:migrate
+        fi
+      '';
+      path = [ cfg.package pkgs.postgresql ];
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+      } // cfgService;
+      after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-streaming = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon streaming";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
+        else { PORT = toString(cfg.streamingPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/run-streaming.sh";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-streaming";
+        RuntimeDirectoryMode = "0750";
+      } // cfgService;
+    };
+
+    systemd.services.mastodon-web = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon web";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-web/web.socket"; }
+        else { PORT = toString(cfg.webPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-web";
+        RuntimeDirectoryMode = "0750";
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    };
+
+    systemd.services.mastodon-sidekiq = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon sidekiq";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // {
+        PORT = toString(cfg.sidekiqPort);
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/sidekiq -c 25 -r ${cfg.package}";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    };
+
+    services.nginx = lib.mkIf cfg.configureNginx {
+      enable = true;
+      recommendedProxySettings = true; # required for redirections to work
+      virtualHosts."${cfg.localDomain}" = {
+        root = "${cfg.package}/public/";
+        forceSSL = true; # mastodon only supports https
+        enableACME = true;
+
+        locations."/system/".alias = "/var/lib/mastodon/public-system/";
+
+        locations."/" = {
+          tryFiles = "$uri @proxy";
+        };
+
+        locations."@proxy" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}");
+          proxyWebsockets = true;
+        };
+
+        locations."/api/v1/streaming/" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/");
+          proxyWebsockets = true;
+        };
+      };
+    };
+
+    services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
+      enable = true;
+    };
+    services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
+      enable = true;
+    };
+    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
+        }
+      ];
+      ensureDatabases = [ cfg.database.name ];
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "mastodon") {
+        mastodon = {
+          isSystemUser = true;
+          home = cfg.package;
+          inherit (cfg) group;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv ])
+    ];
+
+    users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
+  };
+
+  meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
+
+}
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 0a5b6047bb58a..1db1652022a34 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -180,6 +180,7 @@ in
       };
 
       name = mkOption {
+        type = types.str;
         default = "MediaWiki";
         example = "Foobar Wiki";
         description = "Name of the wiki.";
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index 304712d0efc3e..62906d5e6a0ce 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -44,7 +44,7 @@ in
         '';
         description = ''
           Configuration for Miniflux, refer to
-          <link xlink:href="http://docs.miniflux.app/en/latest/configuration.html"/>
+          <link xlink:href="https://miniflux.app/docs/configuration.html"/>
           for documentation on the supported values.
         '';
       };
diff --git a/nixos/modules/services/web-apps/moinmoin.nix b/nixos/modules/services/web-apps/moinmoin.nix
index dc7abce2a5cb5..7a54255a46efc 100644
--- a/nixos/modules/services/web-apps/moinmoin.nix
+++ b/nixos/modules/services/web-apps/moinmoin.nix
@@ -211,7 +211,7 @@ in
             environment = let
               penv = python.buildEnv.override {
                 # setuptools: https://github.com/benoitc/gunicorn/issues/1716
-                extraLibs = [ python.pkgs.gevent python.pkgs.setuptools pkg ];
+                extraLibs = [ python.pkgs.eventlet python.pkgs.setuptools pkg ];
               };
             in {
               PYTHONPATH = "${dataDir}/${wikiIdent}/config:${penv}/${python.sitePackages}";
@@ -224,6 +224,8 @@ in
               chmod -R u+w ${dataDir}/${wikiIdent}/underlay
             '';
 
+            startLimitIntervalSec = 30;
+
             serviceConfig = {
               User = user;
               Group = group;
@@ -231,13 +233,12 @@ in
               ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn moin_wsgi \
                 --name gunicorn-${wikiIdent} \
                 --workers ${toString cfg.gunicorn.workers} \
-                --worker-class gevent \
+                --worker-class eventlet \
                 --bind unix:/run/moin/${wikiIdent}/gunicorn.sock
               '';
 
               Restart = "on-failure";
               RestartSec = "2s";
-              StartLimitIntervalSec = "30s";
 
               StateDirectory = "moin/${wikiIdent}";
               StateDirectoryMode = "0750";
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 8887136ea5e7f..ad1e55d62d1d7 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -84,7 +84,7 @@ in
       type = mkOption {
         type = types.enum [ "mysql" "pgsql" ];
         default = "mysql";
-        description = ''Database engine to use.'';
+        description = "Database engine to use.";
       };
 
       host = mkOption {
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index bad1bd9c767dd..de1c67235f491 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -6,17 +6,19 @@ let
   cfg = config.services.nextcloud;
   fpm = config.services.phpfpm.pools.nextcloud;
 
-  phpPackage =
-    let
-      base = pkgs.php74;
-    in
-      base.buildEnv {
-        extensions = { enabled, all }: with all;
-          enabled ++ [
-            apcu redis memcached imagick
-          ];
-        extraConfig = phpOptionsStr;
-      };
+  phpPackage = pkgs.php74.buildEnv {
+    extensions = { enabled, all }:
+      (with all;
+        enabled
+        ++ [ imagick ] # Always enabled
+        # Optionally enabled depending on caching settings
+        ++ optional cfg.caching.apcu apcu
+        ++ optional cfg.caching.redis redis
+        ++ optional cfg.caching.memcached memcached
+      )
+      ++ cfg.phpExtraExtensions all; # Enabled by user
+    extraConfig = toKeyValue phpOptions;
+  };
 
   toKeyValue = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {} " = ";
@@ -27,7 +29,6 @@ let
     post_max_size = cfg.maxUploadSize;
     memory_limit = cfg.maxUploadSize;
   } // cfg.phpOptions;
-  phpOptionsStr = toKeyValue phpOptions;
 
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
     #! ${pkgs.runtimeShell}
@@ -39,7 +40,7 @@ let
     export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config"
     $sudo \
       ${phpPackage}/bin/php \
-      occ $*
+      occ "$@"
   '';
 
   inherit (config.system) stateVersion;
@@ -116,6 +117,21 @@ in {
       '';
     };
 
+    phpExtraExtensions = mkOption {
+      type = with types; functionTo (listOf package);
+      default = all: [];
+      defaultText = "all: []";
+      description = ''
+        Additional PHP extensions to use for nextcloud.
+        By default, only extensions necessary for a vanilla nextcloud installation are enabled,
+        but you may choose from the list of available extensions and add further ones.
+        This is sometimes necessary to be able to install a certain nextcloud app that has additional requirements.
+      '';
+      example = literalExample ''
+        all: [ all.pdlib all.bz2 ]
+      '';
+    };
+
     phpOptions = mkOption {
       type = types.attrsOf types.str;
       default = {
@@ -228,7 +244,8 @@ in {
         type = types.nullOr types.str;
         default = null;
         description = ''
-          The full path to a file that contains the admin's password.
+          The full path to a file that contains the admin's password. Must be
+          readable by user <literal>nextcloud</literal>.
         '';
       };
 
@@ -351,7 +368,7 @@ in {
         '')
         ++ (optional (versionOlder cfg.package.version "18") (upgradeWarning 17 "20.03"))
         ++ (optional (versionOlder cfg.package.version "19") (upgradeWarning 18 "20.09"))
-        ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.03"));
+        ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.05"));
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -363,6 +380,10 @@ in {
             ''
           else if versionOlder stateVersion "20.03" then nextcloud17
           else if versionOlder stateVersion "20.09" then nextcloud18
+          # 21.03 will not be an official release - it was instead 21.05.
+          # This versionOlder statement remains set to 21.03 for backwards compatibility.
+          # See https://github.com/NixOS/nixpkgs/pull/108899 and
+          # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
           else if versionOlder stateVersion "21.03" then nextcloud19
           else nextcloud20
         );
@@ -391,7 +412,9 @@ in {
                 $file = "${c.dbpassFile}";
                 if (!file_exists($file)) {
                   throw new \RuntimeException(sprintf(
-                    "Cannot start Nextcloud, dbpass file %s set by NixOS doesn't exist!",
+                    "Cannot start Nextcloud, dbpass file %s set by NixOS doesn't seem to "
+                    . "exist! Please make sure that the file exists and has appropriate "
+                    . "permissions for user & group 'nextcloud'!",
                     $file
                   ));
                 }
@@ -464,6 +487,28 @@ in {
           path = [ occ ];
           script = ''
             chmod og+x ${cfg.home}
+
+            ${optionalString (c.dbpassFile != null) ''
+              if [ ! -r "${c.dbpassFile}" ]; then
+                echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+                exit 1
+              fi
+              if [ -z "$(<${c.dbpassFile})" ]; then
+                echo "dbpassFile ${c.dbpassFile} is empty!"
+                exit 1
+              fi
+            ''}
+            ${optionalString (c.adminpassFile != null) ''
+              if [ ! -r "${c.adminpassFile}" ]; then
+                echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+                exit 1
+              fi
+              if [ -z "$(<${c.adminpassFile})" ]; then
+                echo "adminpassFile ${c.adminpassFile} is empty!"
+                exit 1
+              fi
+            ''}
+
             ln -sf ${cfg.package}/apps ${cfg.home}/
 
             # create nextcloud directories.
@@ -509,7 +554,6 @@ in {
         pools.nextcloud = {
           user = "nextcloud";
           group = "nextcloud";
-          phpOptions = phpOptionsStr;
           phpPackage = phpPackage;
           phpEnv = {
             NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config";
@@ -549,9 +593,7 @@ in {
           };
           "/" = {
             priority = 900;
-            extraConfig = if major < 20
-              then "rewrite ^ /index.php;"
-              else "try_files $uri $uri/ /index.php$request_uri;";
+            extraConfig = "rewrite ^ /index.php;";
           };
           "~ ^/store-apps" = {
             priority = 201;
@@ -560,11 +602,12 @@ in {
           "^~ /.well-known" = {
             priority = 210;
             extraConfig = ''
+              absolute_redirect off;
               location = /.well-known/carddav {
-                return 301 $scheme://$host/remote.php/dav;
+                return 301 /remote.php/dav;
               }
               location = /.well-known/caldav {
-                return 301 $scheme://$host/remote.php/dav;
+                return 301 /remote.php/dav;
               }
               try_files $uri $uri/ =404;
             '';
@@ -572,10 +615,10 @@ in {
           "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
             return 404;
           '';
-          "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)".extraConfig = ''
+          "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)".extraConfig = ''
             return 404;
           '';
-          ${if major < 20 then "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" else "~ \\.php(?:$|/)"} = {
+          "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = {
             priority = 500;
             extraConfig = ''
               include ${config.services.nginx.package}/conf/fastcgi.conf;
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index 02e4dba286108..6cbfda118c4f6 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -10,6 +10,10 @@
   <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
+ <para>
+  The current default by NixOS is <package>nextcloud20</package> which is also the latest
+  major version available.
+ </para>
  <section xml:id="module-services-nextcloud-basic-usage">
   <title>Basic usage</title>
 
@@ -178,6 +182,17 @@
   </para>
  </section>
 
+ <section xml:id="installing-apps-php-extensions-nextcloud">
+  <title>Installing Apps and PHP extensions</title>
+
+  <para>
+   Nextcloud apps are installed statefully through the web interface.
+
+   Some apps may require extra PHP extensions to be installed.
+   This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
+  </para>
+ </section>
+
  <section xml:id="module-services-nextcloud-maintainer-info">
   <title>Maintainer information</title>
 
@@ -210,7 +225,7 @@
   nextcloud17 = generic {
     version = "17.0.x";
     sha256 = "0000000000000000000000000000000000000000000000000000";
-    insecure = true;
+    eol = true;
   };
 }</programlisting>
   </para>
diff --git a/nixos/modules/services/web-apps/plantuml-server.nix b/nixos/modules/services/web-apps/plantuml-server.nix
new file mode 100644
index 0000000000000..a39f594c274c4
--- /dev/null
+++ b/nixos/modules/services/web-apps/plantuml-server.nix
@@ -0,0 +1,123 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.plantuml-server;
+
+in
+
+{
+  options = {
+    services.plantuml-server = {
+      enable = mkEnableOption "PlantUML server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.plantuml-server;
+        description = "PlantUML server package to use";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "User which runs PlantUML server.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "Group which runs PlantUML server.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/plantuml";
+        description = "Home directory of the PlantUML server instance.";
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Host to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to listen on.";
+      };
+
+      plantumlLimitSize = mkOption {
+        type = types.int;
+        default = 4096;
+        description = "Limits image width and height.";
+      };
+
+      graphvizPackage = mkOption {
+        type = types.package;
+        default = pkgs.graphviz_2_32;
+        description = "Package containing the dot executable.";
+      };
+
+      plantumlStats = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
+      };
+
+      httpAuthorization = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
+      };
+
+      allowPlantumlInclude = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+      home = cfg.home;
+      createHome = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.services.plantuml-server = {
+      description = "PlantUML server";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.home ];
+      environment = {
+        PLANTUML_LIMIT_SIZE = builtins.toString cfg.plantumlLimitSize;
+        GRAPHVIZ_DOT = "${cfg.graphvizPackage}/bin/dot";
+        PLANTUML_STATS = if cfg.plantumlStats then "on" else "off";
+        HTTP_AUTHORIZATION = cfg.httpAuthorization;
+        ALLOW_PLANTUML_INCLUDE = if cfg.allowPlantumlInclude then "true" else "false";
+      };
+      script = ''
+      ${pkgs.jre}/bin/java \
+        -jar ${pkgs.jetty}/start.jar \
+          --module=deploy,http,jsp \
+          jetty.home=${pkgs.jetty} \
+          jetty.base=${cfg.package} \
+          jetty.http.host=${cfg.listenHost} \
+          jetty.http.port=${builtins.toString cfg.listenPort}
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ truh ];
+}
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
index 1817a2039352d..9083ddfa2206b 100644
--- a/nixos/modules/services/web-apps/shiori.nix
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -37,11 +37,60 @@ in {
       description = "Shiori simple bookmarks manager";
       wantedBy = [ "multi-user.target" ];
 
+      environment.SHIORI_DIR = "/var/lib/shiori";
+
       serviceConfig = {
         ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'";
+
         DynamicUser = true;
-        Environment = "SHIORI_DIR=/var/lib/shiori";
         StateDirectory = "shiori";
+        # As the RootDirectory
+        RuntimeDirectory = "shiori";
+
+        # Security options
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+
+          # For SSL certificates, and the resolv.conf
+          "/etc"
+        ];
+
+        CapabilityBoundingSet = "";
+
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        MemoryDenyWriteExecute = true;
+
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        RootDirectory = "/run/shiori";
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+
+          "~@chown" "~@cpu-emulation" "~@debug" "~@ipc" "~@keyring" "~@memlock"
+          "~@module" "~@obsolete" "~@privileged" "~@process" "~@raw-io"
+          "~@resources" "~@setuid"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
index 3fa8dad049083..3a6ea02676aa5 100644
--- a/nixos/modules/services/web-apps/trilium.nix
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -85,7 +85,7 @@ in
 
   config = lib.mkIf cfg.enable (lib.mkMerge [
   {
-    meta.maintainers = with lib.maintainers; [ kampka ];
+    meta.maintainers = with lib.maintainers; [ ];
 
     users.groups.trilium = {};
     users.users.trilium = {
diff --git a/nixos/modules/services/web-apps/whitebophir.nix b/nixos/modules/services/web-apps/whitebophir.nix
new file mode 100644
index 0000000000000..a19812547c448
--- /dev/null
+++ b/nixos/modules/services/web-apps/whitebophir.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.whitebophir;
+in {
+  options = {
+    services.whitebophir = {
+      enable = mkEnableOption "whitebophir, an online collaborative whiteboard server (persistent state will be maintained under <filename>/var/lib/whitebophir</filename>)";
+
+      package = mkOption {
+        default = pkgs.whitebophir;
+        defaultText = "pkgs.whitebophir";
+        type = types.package;
+        description = "Whitebophir package to use.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5001;
+        description = "Port to bind to.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.whitebophir = {
+      description = "Whitebophir Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+      environment = {
+        PORT            = "${toString cfg.port}";
+        WBO_HISTORY_DIR = "/var/lib/whitebophir";
+      };
+
+      serviceConfig = {
+        DynamicUser    = true;
+        ExecStart      = "${cfg.package}/bin/whitebophir";
+        Restart        = "always";
+        StateDirectory = "whitebophir";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
index 0071951283478..e94861a90b5a8 100644
--- a/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
-  inherit (lib) literalExample mapAttrs optionalString;
+  inherit (lib) literalExample mapAttrs optionalString versionAtLeast;
 
   cfg = config.services.zabbixWeb;
   fpm = config.services.phpfpm.pools.zabbix;
@@ -28,6 +28,8 @@ let
     $ZBX_SERVER_PORT = '${toString cfg.server.port}';
     $ZBX_SERVER_NAME = ''';
     $IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
+
+    ${cfg.extraConfig}
   '';
 
 in
@@ -143,6 +145,14 @@ in
         '';
       };
 
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional configuration to be copied verbatim into <filename>zabbix.conf.php</filename>.
+        '';
+      };
+
     };
   };
 
@@ -150,6 +160,10 @@ in
 
   config = mkIf cfg.enable {
 
+    services.zabbixWeb.extraConfig = optionalString ((versionAtLeast config.system.stateVersion "20.09") && (versionAtLeast cfg.package.version "5.0.0")) ''
+      $DB['DOUBLE_IEEE754'] = 'true';
+    '';
+
     systemd.tmpfiles.rules = [
       "d '${stateDir}' 0750 ${user} ${group} - -"
       "d '${stateDir}/session' 0750 ${user} ${config.services.httpd.group} - -"
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 6ffda3d63614d..7f50b8fd8d441 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -126,6 +126,17 @@ let
     </IfModule>
   '';
 
+  luaSetPaths = let
+    # support both lua and lua.withPackages derivations
+    luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion;
+    in
+  ''
+    <IfModule mod_lua.c>
+      LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so
+      LuaPackagePath  ${cfg.package.lua5}/share/lua/${luaversion}/?.lua
+    </IfModule>
+  '';
+
   mkVHostConf = hostOpts:
     let
       adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
@@ -326,6 +337,8 @@ let
 
     ${sslConf}
 
+    ${optionalString cfg.package.luaSupport luaSetPaths}
+
     # Fascist default - deny access to everything.
     <Directory />
         Options FollowSymLinks
@@ -750,8 +763,8 @@ in
             # Get rid of old semaphores.  These tend to accumulate across
             # server restarts, eventually preventing it from restarting
             # successfully.
-            for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
-                ${pkgs.utillinux}/bin/ipcrm -s $i
+            for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
+                ${pkgs.util-linux}/bin/ipcrm -s $i
             done
           '';
 
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
index 173c0f8561c0f..394f9a305546c 100644
--- a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -112,7 +112,7 @@ in
 
     acmeRoot = mkOption {
       type = types.str;
-      default = "/var/lib/acme/acme-challenges";
+      default = "/var/lib/acme/acme-challenge";
       description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here";
     };
 
diff --git a/nixos/modules/services/web-servers/caddy.nix b/nixos/modules/services/web-servers/caddy.nix
index 72bf9a9a1f448..6ecfc113ca26b 100644
--- a/nixos/modules/services/web-servers/caddy.nix
+++ b/nixos/modules/services/web-servers/caddy.nix
@@ -20,10 +20,30 @@ let
       --config ${configFile} --adapter ${cfg.adapter} > $out
   '';
   tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
-  configJSON = pkgs.runCommand "caddy-config.json" { } ''
-    ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${adaptedConfig} ${tlsJSON} > $out
+
+  # merge the TLS config options we expose with the ones originating in the Caddyfile
+  configJSON =
+    let tlsConfigMerge = ''
+      {"apps":
+        {"tls":
+          {"automation":
+            {"policies":
+              (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
+               then .[0].apps.tls.automation.policies
+               else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
+               end)
+            }
+          }
+        }
+      }'';
+    in pkgs.runCommand "caddy-config.json" { } ''
+    ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
   '';
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
+  ];
+
   options.services.caddy = {
     enable = mkEnableOption "Caddy web server";
 
@@ -66,12 +86,6 @@ in {
       description = "Email address (for Let's Encrypt certificate)";
     };
 
-    agree = mkOption {
-      default = false;
-      type = types.bool;
-      description = "Agree to Let's Encrypt Subscriber Agreement";
-    };
-
     dataDir = mkOption {
       default = "/var/lib/caddy";
       type = types.path;
@@ -103,6 +117,8 @@ in {
       after = [ "network-online.target" ];
       wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service
       wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 14400;
+      startLimitBurst = 10;
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/caddy run --config ${configJSON}";
         ExecReload = "${cfg.package}/bin/caddy reload --config ${configJSON}";
@@ -110,8 +126,6 @@ in {
         User = "caddy";
         Group = "caddy";
         Restart = "on-abnormal";
-        StartLimitIntervalSec = 14400;
-        StartLimitBurst = 10;
         AmbientCapabilities = "cap_net_bind_service";
         CapabilityBoundingSet = "cap_net_bind_service";
         NoNewPrivileges = true;
diff --git a/nixos/modules/services/web-servers/jboss/default.nix b/nixos/modules/services/web-servers/jboss/default.nix
index ca5b8635fc006..d243e0f3f1b78 100644
--- a/nixos/modules/services/web-servers/jboss/default.nix
+++ b/nixos/modules/services/web-servers/jboss/default.nix
@@ -31,32 +31,38 @@ in
 
       tempDir = mkOption {
         default = "/tmp";
+        type = types.str;
         description = "Location where JBoss stores its temp files";
       };
 
       logDir = mkOption {
         default = "/var/log/jboss";
+        type = types.str;
         description = "Location of the logfile directory of JBoss";
       };
 
       serverDir = mkOption {
         description = "Location of the server instance files";
         default = "/var/jboss/server";
+        type = types.str;
       };
 
       deployDir = mkOption {
         description = "Location of the deployment files";
         default = "/nix/var/nix/profiles/default/server/default/deploy/";
+        type = types.str;
       };
 
       libUrl = mkOption {
         default = "file:///nix/var/nix/profiles/default/server/default/lib";
         description = "Location where the shared library JARs are stored";
+        type = types.str;
       };
 
       user = mkOption {
         default = "nobody";
         description = "User account under which jboss runs.";
+        type = types.str;
       };
 
       useJK = mkOption {
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
index 7a3df26e47a6c..d1cb8a8dc2584 100644
--- a/nixos/modules/services/web-servers/lighttpd/default.nix
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -193,7 +193,7 @@ in
       configText = mkOption {
         default = "";
         type = types.lines;
-        example = ''...verbatim config file contents...'';
+        example = "...verbatim config file contents...";
         description = ''
           Overridable config file contents to use for lighttpd. By default, use
           the contents automatically generated by NixOS.
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 39bcb14e5afe8..fa8614e8ec174 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -27,6 +27,33 @@ let
   ) cfg.virtualHosts;
   enableIPv6 = config.networking.enableIPv6;
 
+  defaultFastcgiParams = {
+    SCRIPT_FILENAME   = "$document_root$fastcgi_script_name";
+    QUERY_STRING      = "$query_string";
+    REQUEST_METHOD    = "$request_method";
+    CONTENT_TYPE      = "$content_type";
+    CONTENT_LENGTH    = "$content_length";
+
+    SCRIPT_NAME       = "$fastcgi_script_name";
+    REQUEST_URI       = "$request_uri";
+    DOCUMENT_URI      = "$document_uri";
+    DOCUMENT_ROOT     = "$document_root";
+    SERVER_PROTOCOL   = "$server_protocol";
+    REQUEST_SCHEME    = "$scheme";
+    HTTPS             = "$https if_not_empty";
+
+    GATEWAY_INTERFACE = "CGI/1.1";
+    SERVER_SOFTWARE   = "nginx/$nginx_version";
+
+    REMOTE_ADDR       = "$remote_addr";
+    REMOTE_PORT       = "$remote_port";
+    SERVER_ADDR       = "$server_addr";
+    SERVER_PORT       = "$server_port";
+    SERVER_NAME       = "$server_name";
+
+    REDIRECT_STATUS   = "200";
+  };
+
   recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" ''
     proxy_set_header        Host $host;
     proxy_set_header        X-Real-IP $remote_addr;
@@ -34,7 +61,6 @@ let
     proxy_set_header        X-Forwarded-Proto $scheme;
     proxy_set_header        X-Forwarded-Host $host;
     proxy_set_header        X-Forwarded-Server $host;
-    proxy_set_header        Accept-Encoding "";
   '';
 
   upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
@@ -53,6 +79,8 @@ let
       include ${pkgs.mailcap}/etc/nginx/mime.types;
       include ${cfg.package}/conf/fastcgi.conf;
       include ${cfg.package}/conf/uwsgi_params;
+
+      default_type application/octet-stream;
   '';
 
   configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
@@ -87,7 +115,7 @@ let
       ''}
 
       ssl_protocols ${cfg.sslProtocols};
-      ssl_ciphers ${cfg.sslCiphers};
+      ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
       ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
 
       ${optionalString (cfg.recommendedTlsSettings) ''
@@ -180,6 +208,12 @@ let
       ${cfg.httpConfig}
     }''}
 
+    ${optionalString (cfg.streamConfig != "") ''
+    stream {
+      ${cfg.streamConfig}
+    }
+    ''}
+
     ${cfg.appendConfig}
   '';
 
@@ -262,10 +296,7 @@ let
             ssl_trusted_certificate ${vhost.sslTrustedCertificate};
           ''}
 
-          ${optionalString (vhost.basicAuthFile != null || vhost.basicAuth != {}) ''
-            auth_basic secured;
-            auth_basic_user_file ${if vhost.basicAuthFile != null then vhost.basicAuthFile else mkHtpasswd vhostName vhost.basicAuth};
-          ''}
+          ${mkBasicAuth vhostName vhost}
 
           ${mkLocations vhost.locations}
 
@@ -287,6 +318,10 @@ let
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
       ''}
+      ${concatStringsSep "\n"
+        (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'')
+          (optionalAttrs (config.fastcgiParams != {})
+            (defaultFastcgiParams // config.fastcgiParams)))}
       ${optionalString (config.index != null) "index ${config.index};"}
       ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
       ${optionalString (config.root != null) "root ${config.root};"}
@@ -294,9 +329,19 @@ let
       ${optionalString (config.return != null) "return ${config.return};"}
       ${config.extraConfig}
       ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
+      ${mkBasicAuth "sublocation" config}
     }
   '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
-  mkHtpasswd = vhostName: authDef: pkgs.writeText "${vhostName}.htpasswd" (
+
+  mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let
+    auth_file = if zone.basicAuthFile != null
+      then zone.basicAuthFile
+      else mkHtpasswd name zone.basicAuth;
+  in ''
+    auth_basic secured;
+    auth_basic_user_file ${auth_file};
+  '');
+  mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" (
     concatStringsSep "\n" (mapAttrsToList (user: password: ''
       ${user}:{PLAIN}${password}
     '') authDef)
@@ -361,6 +406,7 @@ in
 
       logError = mkOption {
         default = "stderr";
+        type = types.str;
         description = "
           Configures logging.
           The first parameter defines a file that will store the log. The
@@ -384,13 +430,24 @@ in
       };
 
       config = mkOption {
+        type = types.str;
         default = "";
-        description = "
-          Verbatim nginx.conf configuration.
-          This is mutually exclusive with the structured configuration
-          via virtualHosts and the recommendedXyzSettings configuration
-          options. See appendConfig for appending to the generated http block.
-        ";
+        description = ''
+          Verbatim <filename>nginx.conf</filename> configuration.
+          This is mutually exclusive to any other config option for
+          <filename>nginx.conf</filename> except for
+          <itemizedlist>
+          <listitem><para><xref linkend="opt-services.nginx.appendConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.httpConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.logError" />
+          </para></listitem>
+          </itemizedlist>
+
+          If additional verbatim config in addition to other options is needed,
+          <xref linkend="opt-services.nginx.appendConfig" /> should be used instead.
+        '';
       };
 
       appendConfig = mkOption {
@@ -435,6 +492,21 @@ in
         ";
       };
 
+      streamConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          server {
+            listen 127.0.0.1:53 udp reuseport;
+            proxy_timeout 20s;
+            proxy_pass 192.168.0.1:53535;
+          }
+        '';
+        description = "
+          Configuration lines to be set inside the stream block.
+        ";
+      };
+
       eventsConfig = mkOption {
         type = types.lines;
         default = "";
@@ -488,7 +560,7 @@ in
       };
 
       sslCiphers = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
         default = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
         description = "Ciphers to choose from when negotiating TLS handshakes.";
@@ -694,6 +766,8 @@ in
         ${cfg.preStart}
         ${execCommand} -t
       '';
+
+      startLimitIntervalSec = 60;
       serviceConfig = {
         ExecStart = execCommand;
         ExecReload = [
@@ -702,7 +776,6 @@ in
         ];
         Restart = "always";
         RestartSec = "10s";
-        StartLimitInterval = "1min";
         # User and group
         User = cfg.user;
         Group = cfg.group;
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 3d9e391ecf20f..d8c976f202fd1 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -9,6 +9,34 @@ with lib;
 
 {
   options = {
+    basicAuth = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExample ''
+        {
+          user = "password";
+        };
+      '';
+      description = ''
+        Basic Auth protection for a vhost.
+
+        WARNING: This is implemented to store the password in plain text in the
+        Nix store.
+      '';
+    };
+
+    basicAuthFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Basic Auth password file for a vhost.
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
+      '';
+    };
+
     proxyPass = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -24,7 +52,7 @@ with lib;
       default = false;
       example = true;
       description = ''
-        Whether to supporty proxying websocket connections with HTTP/1.1.
+        Whether to support proxying websocket connections with HTTP/1.1.
       '';
     };
 
@@ -73,6 +101,16 @@ with lib;
       '';
     };
 
+    fastcgiParams = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      description = ''
+        FastCGI parameters to override.  Unlike in the Nginx
+        configuration file, overriding only some default parameters
+        won't unset the default values for other parameters.
+      '';
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       default = "";
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 455854e2a9655..cf211ea9a71b6 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -198,7 +198,7 @@ with lib;
         Basic Auth protection for a vhost.
 
         WARNING: This is implemented to store the password in plain text in the
-        nix store.
+        Nix store.
       '';
     };
 
@@ -207,7 +207,10 @@ with lib;
       default = null;
       description = ''
         Basic Auth password file for a vhost.
-        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
       '';
     };
 
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index 6d12925829f78..13fe98402c60e 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -74,6 +74,7 @@ in
 
       extraGroups = mkOption {
         default = [];
+        type = types.listOf types.str;
         example = [ "users" ];
         description = "Defines extra groups to which the tomcat user belongs.";
       };
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index 4ab7307c3b671..3d29199dd4549 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -136,6 +136,8 @@ in {
       description = "Traefik web server";
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 86400;
+      startLimitBurst = 5;
       serviceConfig = {
         ExecStart =
           "${cfg.package}/bin/traefik --configfile=${staticConfigFile}";
@@ -143,8 +145,6 @@ in {
         User = "traefik";
         Group = cfg.group;
         Restart = "on-failure";
-        StartLimitInterval = 86400;
-        StartLimitBurst = 5;
         AmbientCapabilities = "cap_net_bind_service";
         CapabilityBoundingSet = "cap_net_bind_service";
         NoNewPrivileges = true;
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 894271d1e55e4..2a264bf2e9a6f 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -28,10 +28,12 @@ in {
         description = "Group account under which unit runs.";
       };
       stateDir = mkOption {
+        type = types.path;
         default = "/var/spool/unit";
         description = "Unit data directory.";
       };
       logDir = mkOption {
+        type = types.path;
         default = "/var/log/unit";
         description = "Unit log directory.";
       };
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index 936e211ec7138..2dfc39c847aa8 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -5,11 +5,24 @@ with lib;
 let
   cfg = config.services.uwsgi;
 
+  isEmperor = cfg.instance.type == "emperor";
+
+  imperialPowers =
+    [
+      # spawn other user processes
+      "CAP_SETUID" "CAP_SETGID"
+      "CAP_SYS_CHROOT"
+      # transfer capabilities
+      "CAP_SETPCAP"
+      # create other user sockets
+      "CAP_CHOWN"
+    ];
+
   buildCfg = name: c:
     let
       plugins =
         if any (n: !any (m: m == n) cfg.plugins) (c.plugins or [])
-        then throw "`plugins` attribute in UWSGI configuration contains plugins not in config.services.uwsgi.plugins"
+        then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins"
         else c.plugins or cfg.plugins;
 
       hasPython = v: filter (n: n == "python${v}") plugins != [];
@@ -18,7 +31,7 @@ let
 
       python =
         if hasPython2 && hasPython3 then
-          throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
+          throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3"
         else if hasPython2 then cfg.package.python2
         else if hasPython3 then cfg.package.python3
         else null;
@@ -43,7 +56,7 @@ let
                       oldPaths = filter (x: x != null) (map getPath env');
                   in env' ++ [ "PATH=${optionalString (oldPaths != []) "${last oldPaths}:"}${pythonEnv}/bin" ];
               }
-          else if c.type == "emperor"
+          else if isEmperor
             then {
               emperor = if builtins.typeOf c.vassals != "set" then c.vassals
                         else pkgs.buildEnv {
@@ -51,7 +64,7 @@ let
                           paths = mapAttrsToList buildCfg c.vassals;
                         };
             } // removeAttrs c [ "type" "vassals" ]
-          else throw "`type` attribute in UWSGI configuration should be either 'normal' or 'emperor'";
+          else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'";
       };
 
     in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg);
@@ -79,7 +92,7 @@ in {
       };
 
       instance = mkOption {
-        type =  with lib.types; let
+        type =  with types; let
           valueType = nullOr (oneOf [
             bool
             int
@@ -137,31 +150,66 @@ in {
       user = mkOption {
         type = types.str;
         default = "uwsgi";
-        description = "User account under which uwsgi runs.";
+        description = "User account under which uWSGI runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "uwsgi";
-        description = "Group account under which uwsgi runs.";
+        description = "Group account under which uWSGI runs.";
+      };
+
+      capabilities = mkOption {
+        type = types.listOf types.str;
+        apply = caps: caps ++ optionals isEmperor imperialPowers;
+        default = [ ];
+        example = literalExample ''
+          [
+            "CAP_NET_BIND_SERVICE" # bind on ports <1024
+            "CAP_NET_RAW"          # open raw sockets
+          ]
+        '';
+        description = ''
+          Grant capabilities to the uWSGI instance. See the
+          <literal>capabilities(7)</literal> for available values.
+          <note>
+            <para>
+              uWSGI runs as an unprivileged user (even as Emperor) with the minimal
+              capabilities required. This option can be used to add fine-grained
+              permissions without running the service as root.
+            </para>
+            <para>
+              When in Emperor mode, any capability to be inherited by a vassal must
+              be specified again in the vassal configuration using <literal>cap</literal>.
+              See the uWSGI <link
+              xlink:href="https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html">docs</link>
+              for more information.
+            </para>
+          </note>
+        '';
       };
     };
   };
 
   config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") ''
+      d ${cfg.runDir} 775 ${cfg.user} ${cfg.group}
+    '';
+
     systemd.services.uwsgi = {
       wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -p ${cfg.runDir}
-        chown ${cfg.user}:${cfg.group} ${cfg.runDir}
-      '';
       serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
         Type = "notify";
-        ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         NotifyAccess = "main";
         KillSignal = "SIGQUIT";
+        AmbientCapabilities = cfg.capabilities;
+        CapabilityBoundingSet = cfg.capabilities;
+        RuntimeDirectory = mkIf (cfg.runDir == "/run/uwsgi") "uwsgi";
       };
     };
 
diff --git a/nixos/modules/services/x11/clight.nix b/nixos/modules/services/x11/clight.nix
index 4daf6d8d9db7e..873f425fb8be4 100644
--- a/nixos/modules/services/x11/clight.nix
+++ b/nixos/modules/services/x11/clight.nix
@@ -11,14 +11,21 @@ let
     else if isBool v      then boolToString v
     else if isString v    then ''"${escape [''"''] v}"''
     else if isList v      then "[ " + concatMapStringsSep ", " toConf v + " ]"
+    else if isAttrs v     then "\n{\n" + convertAttrs v + "\n}"
     else abort "clight.toConf: unexpected type (v = ${v})";
 
-  clightConf = pkgs.writeText "clight.conf"
-    (concatStringsSep "\n" (mapAttrsToList
-      (name: value: "${toString name} = ${toConf value};")
-      (filterAttrs
-        (_: value: value != null)
-        cfg.settings)));
+  getSep = v:
+    if isAttrs v then ":"
+    else "=";
+
+  convertAttrs = attrs: concatStringsSep "\n" (mapAttrsToList
+    (name: value: "${toString name} ${getSep value} ${toConf value};")
+    attrs);
+
+  clightConf = pkgs.writeText "clight.conf" (convertAttrs
+    (filterAttrs
+      (_: value: value != null)
+      cfg.settings));
 in {
   options.services.clight = {
     enable = mkOption {
@@ -49,9 +56,10 @@ in {
     };
 
     settings = let
-      validConfigTypes = with types; either int (either str (either bool float));
+      validConfigTypes = with types; oneOf [ int str bool float ];
+      collectionTypes = with types; oneOf [ validConfigTypes (listOf validConfigTypes) ];
     in mkOption {
-      type = with types; attrsOf (nullOr (either validConfigTypes (listOf validConfigTypes)));
+      type = with types; attrsOf (nullOr (either collectionTypes (attrsOf collectionTypes)));
       default = {};
       example = { captures = 20; gamma_long_transition = true; ac_capture_timeouts = [ 120 300 60 ]; };
       description = ''
@@ -69,10 +77,10 @@ in {
     services.upower.enable = true;
 
     services.clight.settings = {
-      gamma_temp = with cfg.temperature; mkDefault [ day night ];
+      gamma.temp = with cfg.temperature; mkDefault [ day night ];
     } // (optionalAttrs (config.location.provider == "manual") {
-      latitude = mkDefault config.location.latitude;
-      longitude = mkDefault config.location.longitude;
+      daytime.latitude = mkDefault config.location.latitude;
+      daytime.longitude = mkDefault config.location.longitude;
     });
 
     services.geoclue2.appConfig.clightc = {
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index a404143a03d4b..14dcf009a7d13 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -25,6 +25,7 @@ in
 
       sessionPath = mkOption {
         default = [];
+        type = types.listOf types.package;
         example = literalExample "[ pkgs.gnome3.gpaste ]";
         description = ''
           Additional list of packages to be added to the session search path.
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index acccbdb9950a3..99e6edfba26e2 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -17,6 +17,11 @@ let
     '';
   };
 
+  defaultFavoriteAppsOverride = ''
+    [org.gnome.shell]
+    favorite-apps=[ 'org.gnome.Epiphany.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop' ]
+  '';
+
   nixos-gsettings-desktop-schemas = let
     defaultPackages = with pkgs; [ gsettings-desktop-schemas gnome3.gnome-shell ];
   in
@@ -42,8 +47,7 @@ let
        [org.gnome.desktop.screensaver]
        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' ]
+       ${cfg.favoriteAppsOverride}
 
        ${cfg.extraGSettingsOverrides}
      EOF
@@ -69,6 +73,7 @@ in
       core-os-services.enable = mkEnableOption "essential services for GNOME3";
       core-shell.enable = mkEnableOption "GNOME Shell services";
       core-utilities.enable = mkEnableOption "GNOME core utilities";
+      core-developer-tools.enable = mkEnableOption "GNOME core developer tools";
       games.enable = mkEnableOption "GNOME games";
 
       experimental-features = {
@@ -113,6 +118,7 @@ in
 
       sessionPath = mkOption {
         default = [];
+        type = types.listOf types.package;
         example = literalExample "[ pkgs.gnome3.gpaste ]";
         description = ''
           Additional list of packages to be added to the session search path.
@@ -123,6 +129,17 @@ in
         apply = list: list ++ [ pkgs.gnome3.gnome-shell pkgs.gnome3.gnome-shell-extensions ];
       };
 
+      favoriteAppsOverride = mkOption {
+        internal = true; # this is messy
+        default = defaultFavoriteAppsOverride;
+        type = types.lines;
+        example = literalExample ''
+          [org.gnome.shell]
+          favorite-apps=[ 'firefox.desktop', 'org.gnome.Calendar.desktop' ]
+        '';
+        description = "List of desktop files to put as favorite apps into gnome-shell. These need to be installed somehow globally.";
+      };
+
       extraGSettingsOverrides = mkOption {
         default = "";
         type = types.lines;
@@ -179,6 +196,13 @@ in
 
   config = mkMerge [
     (mkIf (cfg.enable || flashbackEnabled) {
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [''
+        # Enable the GNOME 3 Desktop Environment.
+        services.xserver.displayManager.gdm.enable = true;
+        services.xserver.desktopManager.gnome3.enable = true;
+      ''];
+
       services.gnome3.core-os-services.enable = true;
       services.gnome3.core-shell.enable = true;
       services.gnome3.core-utilities.enable = mkDefault true;
@@ -207,6 +231,11 @@ in
 
        # If gnome3 is installed, build vim for gtk3 too.
       nixpkgs.config.vim.gui = "gtk3";
+
+      # Install gnome-software if flatpak is enabled
+      services.flatpak.guiPackages = [
+        pkgs.gnome3.gnome-software
+      ];
     })
 
     (mkIf flashbackEnabled {
@@ -294,6 +323,12 @@ in
         gnome-shell
       ];
 
+      services.udev.packages = with pkgs.gnome3; [
+        # Force enable KMS modifiers for devices that require them.
+        # https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1443
+        mutter
+      ];
+
       services.avahi.enable = mkDefault true;
 
       xdg.portal.extraPortals = [
@@ -323,7 +358,7 @@ in
         source-sans-pro
       ];
 
-      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-36/elements/core/meta-gnome-core-shell.bst
+      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-shell.bst
       environment.systemPackages = with pkgs.gnome3; [
         adwaita-icon-theme
         gnome-backgrounds
@@ -368,15 +403,13 @@ in
       };
     })
 
-    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-36/elements/core/meta-gnome-core-utilities.bst
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-utilities.bst
     (mkIf serviceCfg.core-utilities.enable {
       environment.systemPackages = (with pkgs.gnome3; removePackagesByName [
         baobab
         cheese
         eog
-        /* Not in good standing on nixos:
-         * https://github.com/NixOS/nixpkgs/issues/98819
-        /* epiphany */
+        epiphany
         gedit
         gnome-calculator
         gnome-calendar
@@ -387,17 +420,15 @@ in
         gnome-logs
         gnome-maps
         gnome-music
-        gnome-photos
+        pkgs.gnome-photos
         gnome-screenshot
-        gnome-software
         gnome-system-monitor
         gnome-weather
         nautilus
+        pkgs.gnome-connections
         simple-scan
         totem
         yelp
-        # Unsure if sensible for NixOS
-        /* gnome-boxes */
       ] config.environment.gnome3.excludePackages);
 
       # Enable default program modules
@@ -426,11 +457,42 @@ in
 
     (mkIf serviceCfg.games.enable {
       environment.systemPackages = (with pkgs.gnome3; removePackagesByName [
-        aisleriot atomix five-or-more four-in-a-row gnome-chess gnome-klotski
-        gnome-mahjongg gnome-mines gnome-nibbles gnome-robots gnome-sudoku
-        gnome-taquin gnome-tetravex hitori iagno lightsoff quadrapassel
-        swell-foop tali
+        aisleriot
+        atomix
+        five-or-more
+        four-in-a-row
+        gnome-chess
+        gnome-klotski
+        gnome-mahjongg
+        gnome-mines
+        gnome-nibbles
+        gnome-robots
+        gnome-sudoku
+        gnome-taquin
+        gnome-tetravex
+        hitori
+        iagno
+        lightsoff
+        quadrapassel
+        swell-foop
+        tali
+      ] config.environment.gnome3.excludePackages);
+    })
+
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/-/blob/3.38.0/elements/core/meta-gnome-core-developer-tools.bst
+    (mkIf serviceCfg.core-developer-tools.enable {
+      environment.systemPackages = (with pkgs.gnome3; removePackagesByName [
+        dconf-editor
+        devhelp
+        pkgs.gnome-builder
+        # boxes would make sense in this option, however
+        # it doesn't function well enough to be included
+        # in default configurations.
+        # https://github.com/NixOS/nixpkgs/issues/60908
+        /* gnome-boxes */
       ] config.environment.gnome3.excludePackages);
+
+      services.sysprof.enable = true;
     })
   ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index e67e216f90d90..195da75e74437 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -42,6 +42,7 @@ in
 
       sessionPath = mkOption {
         default = [];
+        type = types.listOf types.package;
         example = literalExample "[ pkgs.gnome3.gpaste ]";
         description = ''
           Additional list of packages to be added to the session search path.
@@ -180,7 +181,6 @@ 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 e48b5f23b58fe..44ee079b81737 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -7,9 +7,8 @@ let
   xcfg = config.services.xserver;
   cfg = xcfg.desktopManager.plasma5;
 
-  inherit (pkgs) kdeApplications kdeFrameworks plasma5;
-  libsForQt5 = pkgs.libsForQt514;
-  qt5 = pkgs.qt514;
+  libsForQt5 = pkgs.plasma5Packages;
+  inherit (libsForQt5) kdeApplications kdeFrameworks plasma5;
   inherit (pkgs) writeText;
 
   pulseaudio = config.hardware.pulseaudio;
@@ -184,6 +183,13 @@ in
 
   config = mkMerge [
     (mkIf cfg.enable {
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [''
+        # Enable the Plasma 5 Desktop Environment.
+        services.xserver.displayManager.sddm.enable = true;
+        services.xserver.desktopManager.plasma5.enable = true;
+      ''];
+
       services.xserver.desktopManager.session = singleton {
         name = "plasma5";
         bgSupport = true;
@@ -191,8 +197,8 @@ in
       };
 
       security.wrappers = {
-        kcheckpass.source = "${lib.getBin plasma5.kscreenlocker}/libexec/kcheckpass";
-        start_kdeinit.source = "${lib.getBin pkgs.kinit}/libexec/kf5/start_kdeinit";
+        kcheckpass.source = "${lib.getBin libsForQt5.kscreenlocker}/libexec/kcheckpass";
+        start_kdeinit.source = "${lib.getBin libsForQt5.kinit}/libexec/kf5/start_kdeinit";
         kwin_wayland = {
           source = "${lib.getBin plasma5.kwin}/bin/kwin_wayland";
           capabilities = "cap_sys_nice+ep";
@@ -206,7 +212,7 @@ in
       '';
 
       environment.systemPackages =
-        with qt5; with libsForQt5;
+        with libsForQt5;
         with plasma5; with kdeApplications; with kdeFrameworks;
         [
           frameworkintegration
@@ -230,6 +236,7 @@ in
           kidletime
           kimageformats
           kinit
+          kirigami2  # In system profile for SDDM theme. TODO: wrapper.
           kio
           kjobwidgets
           knewstuff
@@ -359,7 +366,7 @@ in
       security.pam.services.sddm.enableKwallet = true;
 
       xdg.portal.enable = true;
-      xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-kde ];
+      xdg.portal.extraPortals = [ plasma5.xdg-desktop-portal-kde ];
 
       # Update the start menu for each user that is currently logged in
       system.userActivationScripts.plasmaSetup = activationScript;
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 8ac7a9e66c6c3..9fdbe753dad50 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -37,13 +37,6 @@ let
       . /etc/profile
       cd "$HOME"
 
-      ${optionalString cfg.startDbusSession ''
-        if test -z "$DBUS_SESSION_BUS_ADDRESS"; then
-          /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
-      ''}
-
       ${optionalString cfg.displayManager.job.logToJournal ''
         if [ -z "$_DID_SYSTEMD_CAT" ]; then
           export _DID_SYSTEMD_CAT=1
@@ -451,8 +444,8 @@ in
       in
         # We will generate every possible pair of WM and DM.
         concatLists (
-          crossLists
-            (dm: wm: let
+            builtins.map
+            ({dm, wm}: let
               sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
               script = xsession dm wm;
               desktopNames = if dm ? desktopNames
@@ -479,14 +472,13 @@ in
                   providedSessions = [ sessionName ];
                 })
             )
-            [dms wms]
+            (cartesianProductOfSets { dm = dms; wm = wms; })
           );
 
-    # Make xsessions and wayland sessions installed at
-    # /run/current-system/sw/share as some programs
-    # have behavior that depends on them being installed
-    environment.systemPackages = [
-      cfg.displayManager.sessionData.desktops
+    # Make xsessions and wayland sessions available in XDG_DATA_DIRS
+    # as some programs have behavior that depends on them being present
+    environment.sessionVariables.XDG_DATA_DIRS = [
+      "${cfg.displayManager.sessionData.desktops}/share"
     ];
   };
 
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index f53a4a49a06aa..f79eb64b5a6a2 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -160,7 +160,7 @@ in
     ];
 
     # Otherwise GDM will not be able to start correctly and display Wayland sessions
-    systemd.packages = with pkgs.gnome3; [ gnome-session gnome-shell ];
+    systemd.packages = with pkgs.gnome3; [ gdm gnome-session gnome-shell ];
     environment.systemPackages = [ pkgs.gnome3.adwaita-icon-theme ];
 
     systemd.services.display-manager.wants = [
@@ -270,7 +270,7 @@ in
     # presented and there's a little delay.
     environment.etc."gdm/custom.conf".text = ''
       [daemon]
-      WaylandEnable=${if cfg.gdm.wayland then "true" else "false"}
+      WaylandEnable=${boolToString cfg.gdm.wayland}
       ${optionalString cfg.autoLogin.enable (
         if cfg.gdm.autoLogin.delay > 0 then ''
           TimedLoginEnable=true
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
index de932e6e840ae..9c1dc1d1c12d9 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
@@ -88,6 +88,7 @@ in
       cursorTheme = {
 
         package = mkOption {
+          type = types.package;
           default = pkgs.gnome3.adwaita-icon-theme;
           defaultText = "pkgs.gnome3.adwaita-icon-theme";
           description = ''
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 143785db0b4fc..2dafee9e36e3d 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -308,6 +308,7 @@ in
       home = "/var/lib/lightdm";
       group = "lightdm";
       uid = config.ids.uids.lightdm;
+      shell = pkgs.bash;
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index e63bb2e445396..116994db1c140 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -1,97 +1,94 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   xcfg = config.services.xserver;
   dmcfg = xcfg.displayManager;
   cfg = dmcfg.sddm;
   xEnv = config.systemd.services.display-manager.environment;
 
-  inherit (pkgs) sddm;
+  sddm = pkgs.libsForQt5.sddm;
+
+  iniFmt = pkgs.formats.ini { };
 
-  xserverWrapper = pkgs.writeScript "xserver-wrapper" ''
-    #!/bin/sh
+  xserverWrapper = pkgs.writeShellScript "xserver-wrapper" ''
     ${concatMapStrings (n: "export ${n}=\"${getAttr n xEnv}\"\n") (attrNames xEnv)}
     exec systemd-cat -t xserver-wrapper ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} "$@"
   '';
 
-  Xsetup = pkgs.writeScript "Xsetup" ''
-    #!/bin/sh
+  Xsetup = pkgs.writeShellScript "Xsetup" ''
     ${cfg.setupScript}
     ${dmcfg.setupCommands}
   '';
 
-  Xstop = pkgs.writeScript "Xstop" ''
-    #!/bin/sh
+  Xstop = pkgs.writeShellScript "Xstop" ''
     ${cfg.stopScript}
   '';
 
-  cfgFile = pkgs.writeText "sddm.conf" ''
-    [General]
-    HaltCommand=/run/current-system/systemd/bin/systemctl poweroff
-    RebootCommand=/run/current-system/systemd/bin/systemctl reboot
-    ${optionalString cfg.autoNumlock ''
-    Numlock=on
-    ''}
-
-    [Theme]
-    Current=${cfg.theme}
-    ThemeDir=/run/current-system/sw/share/sddm/themes
-    FacesDir=/run/current-system/sw/share/sddm/faces
-
-    [Users]
-    MaximumUid=${toString config.ids.uids.nixbld}
-    HideUsers=${concatStringsSep "," dmcfg.hiddenUsers}
-    HideShells=/run/current-system/sw/bin/nologin
-
-    [X11]
-    MinimumVT=${toString (if xcfg.tty != null then xcfg.tty else 7)}
-    ServerPath=${xserverWrapper}
-    XephyrPath=${pkgs.xorg.xorgserver.out}/bin/Xephyr
-    SessionCommand=${dmcfg.sessionData.wrapper}
-    SessionDir=${dmcfg.sessionData.desktops}/share/xsessions
-    XauthPath=${pkgs.xorg.xauth}/bin/xauth
-    DisplayCommand=${Xsetup}
-    DisplayStopCommand=${Xstop}
-    EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-
-    [Wayland]
-    EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-    SessionDir=${dmcfg.sessionData.desktops}/share/wayland-sessions
-
-    ${optionalString dmcfg.autoLogin.enable ''
-    [Autologin]
-    User=${dmcfg.autoLogin.user}
-    Session=${autoLoginSessionName}.desktop
-    Relogin=${boolToString cfg.autoLogin.relogin}
-    ''}
-
-    ${cfg.extraConfig}
-  '';
+  defaultConfig = {
+    General = {
+      HaltCommand = "/run/current-system/systemd/bin/systemctl poweroff";
+      RebootCommand = "/run/current-system/systemd/bin/systemctl reboot";
+      Numlock = if cfg.autoNumlock then "on" else "none"; # on, off none
+    };
+
+    Theme = {
+      Current = cfg.theme;
+      ThemeDir = "/run/current-system/sw/share/sddm/themes";
+      FacesDir = "/run/current-system/sw/share/sddm/faces";
+    };
+
+    Users = {
+      MaximumUid = config.ids.uids.nixbld;
+      HideUsers = concatStringsSep "," dmcfg.hiddenUsers;
+      HideShells = "/run/current-system/sw/bin/nologin";
+    };
 
-  autoLoginSessionName = dmcfg.sessionData.autologinSession;
+    X11 = {
+      MinimumVT = if xcfg.tty != null then xcfg.tty else 7;
+      ServerPath = toString xserverWrapper;
+      XephyrPath = "${pkgs.xorg.xorgserver.out}/bin/Xephyr";
+      SessionCommand = toString dmcfg.sessionData.wrapper;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/xsessions";
+      XauthPath = "${pkgs.xorg.xauth}/bin/xauth";
+      DisplayCommand = toString Xsetup;
+      DisplayStopCommand = toString Xstop;
+      EnableHiDPI = cfg.enableHidpi;
+    };
+
+    Wayland = {
+      EnableHiDPI = cfg.enableHidpi;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/wayland-sessions";
+    };
+  } // lib.optionalAttrs dmcfg.autoLogin.enable {
+    Autologin = {
+      User = dmcfg.autoLogin.user;
+      Session = autoLoginSessionName;
+      Relogin = cfg.autoLogin.relogin;
+    };
+  };
+
+  cfgFile =
+    iniFmt.generate "sddm.conf" (lib.recursiveUpdate defaultConfig cfg.settings);
+
+  autoLoginSessionName =
+    "${dmcfg.sessionData.autologinSession}.desktop";
 
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "sddm" "themes" ]
+    (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"
-    ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "enable" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "enable" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "user" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "user" ])
+    (mkRemovedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "extraConfig" ]
+      "Set the option `services.xserver.displayManager.sddm.settings' instead.")
   ];
 
   options = {
@@ -110,22 +107,22 @@ in
         default = true;
         description = ''
           Whether to enable automatic HiDPI mode.
-          </para>
-          <para>
-          Versions up to 0.17 are broken so this only works from 0.18 onwards.
         '';
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
+      settings = mkOption {
+        type = iniFmt.type;
+        default = { };
         example = ''
-          [Autologin]
-          User=john
-          Session=plasma.desktop
+          {
+            Autologin = {
+              User = "john";
+              Session = "plasma.desktop";
+            };
+          }
         '';
         description = ''
-          Extra lines appended to the configuration of SDDM.
+          Extra settings merged in and overwritting defaults in sddm.conf.
         '';
       };
 
@@ -168,28 +165,38 @@ in
       };
 
       # Configuration for automatic login specific to SDDM
-      autoLogin.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.
-        '';
+      autoLogin = {
+        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.
+          '';
+        };
+
+        minimumUid = mkOption {
+          type = types.ints.u16;
+          default = 1000;
+          description = ''
+            Minimum user ID for auto-login user.
+          '';
+        };
       };
-
     };
-
   };
 
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = xcfg.enable;
+      {
+        assertion = xcfg.enable;
         message = ''
           SDDM requires services.xserver.enable to be true
         '';
       }
-      { assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
+      {
+        assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
         message = ''
           SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
@@ -230,7 +237,7 @@ in
 
       sddm-autologin.text = ''
         auth     requisite pam_nologin.so
-        auth     required  pam_succeed_if.so uid >= 1000 quiet
+        auth     required  pam_succeed_if.so uid >= ${toString cfg.autoLogin.minimumUid} quiet
         auth     required  pam_permit.so
 
         account  include   sddm
diff --git a/nixos/modules/services/x11/display-managers/startx.nix b/nixos/modules/services/x11/display-managers/startx.nix
index 3980203b9457f..6cd46cdf96498 100644
--- a/nixos/modules/services/x11/display-managers/startx.nix
+++ b/nixos/modules/services/x11/display-managers/startx.nix
@@ -39,6 +39,18 @@ in
       displayManager.lightdm.enable = lib.mkForce false;
     };
     systemd.services.display-manager.enable = false;
+
+    # Other displayManagers log to /dev/null because they're services and put
+    # Xorg's stdout in the journal
+    #
+    # To send log to Xorg's default log location ($XDG_DATA_HOME/xorg/), we do
+    # not specify a log file when running X
+    services.xserver.logFile = mkDefault null;
+
+    # Implement xserverArgs via xinit's system-wide xserverrc
+    environment.etc."X11/xinit/xserverrc".source = pkgs.writeShellScript "xserverrc" ''
+      exec ${pkgs.xorg.xorgserver}/bin/X ${toString config.services.xserver.displayManager.xserverArgs} "$@"
+    '';
     environment.systemPackages =  with pkgs; [ xorg.xinit ];
   };
 
diff --git a/nixos/modules/services/x11/hardware/libinput.nix b/nixos/modules/services/x11/hardware/libinput.nix
index 9548ecb8ef6da..9b0757153cc2f 100644
--- a/nixos/modules/services/x11/hardware/libinput.nix
+++ b/nixos/modules/services/x11/hardware/libinput.nix
@@ -3,23 +3,18 @@
 with lib;
 
 let cfg = config.services.xserver.libinput;
-    xorgBool = v: if v then "on" else "off";
-in {
 
-  options = {
-
-    services.xserver.libinput = {
-
-      enable = mkEnableOption "libinput";
+    xorgBool = v: if v then "on" else "off";
 
+    mkConfigForDevice = deviceType: {
       dev = mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "/dev/input/event0";
         description =
           ''
-            Path for touchpad device.  Set to null to apply to any
-            auto-detected touchpad.
+            Path for ${deviceType} device.  Set to null to apply to any
+            auto-detected ${deviceType}.
           '';
       };
 
@@ -185,14 +180,64 @@ in {
           Option "DragLockButtons" "L1 B1 L2 B2"
         '';
         description = ''
-          Additional options for libinput touchpad driver. See
+          Additional options for libinput ${deviceType} driver. See
           <citerefentry><refentrytitle>libinput</refentrytitle><manvolnum>4</manvolnum></citerefentry>
           for available options.";
         '';
       };
-
     };
 
+    mkX11ConfigForDevice = deviceType: matchIs: ''
+        Identifier "libinput ${deviceType} configuration"
+        MatchDriver "libinput"
+        MatchIs${matchIs} "${xorgBool true}"
+        ${optionalString (cfg.${deviceType}.dev != null) ''MatchDevicePath "${cfg.${deviceType}.dev}"''}
+        Option "AccelProfile" "${cfg.${deviceType}.accelProfile}"
+        ${optionalString (cfg.${deviceType}.accelSpeed != null) ''Option "AccelSpeed" "${cfg.${deviceType}.accelSpeed}"''}
+        ${optionalString (cfg.${deviceType}.buttonMapping != null) ''Option "ButtonMapping" "${cfg.${deviceType}.buttonMapping}"''}
+        ${optionalString (cfg.${deviceType}.calibrationMatrix != null) ''Option "CalibrationMatrix" "${cfg.${deviceType}.calibrationMatrix}"''}
+        ${optionalString (cfg.${deviceType}.clickMethod != null) ''Option "ClickMethod" "${cfg.${deviceType}.clickMethod}"''}
+        Option "LeftHanded" "${xorgBool cfg.${deviceType}.leftHanded}"
+        Option "MiddleEmulation" "${xorgBool cfg.${deviceType}.middleEmulation}"
+        Option "NaturalScrolling" "${xorgBool cfg.${deviceType}.naturalScrolling}"
+        ${optionalString (cfg.${deviceType}.scrollButton != null) ''Option "ScrollButton" "${toString cfg.${deviceType}.scrollButton}"''}
+        Option "ScrollMethod" "${cfg.${deviceType}.scrollMethod}"
+        Option "HorizontalScrolling" "${xorgBool cfg.${deviceType}.horizontalScrolling}"
+        Option "SendEventsMode" "${cfg.${deviceType}.sendEventsMode}"
+        Option "Tapping" "${xorgBool cfg.${deviceType}.tapping}"
+        Option "TappingDragLock" "${xorgBool cfg.${deviceType}.tappingDragLock}"
+        Option "DisableWhileTyping" "${xorgBool cfg.${deviceType}.disableWhileTyping}"
+        ${cfg.${deviceType}.additionalOptions}
+  '';
+in {
+
+  imports =
+    (map (option: mkRenamedOptionModule ([ "services" "xserver" "libinput" option ]) [ "services" "xserver" "libinput" "touchpad" option ]) [
+      "accelProfile"
+      "accelSpeed"
+      "buttonMapping"
+      "calibrationMatrix"
+      "clickMethod"
+      "leftHanded"
+      "middleEmulation"
+      "naturalScrolling"
+      "scrollButton"
+      "scrollMethod"
+      "horizontalScrolling"
+      "sendEventsMode"
+      "tapping"
+      "tappingDragLock"
+      "disableWhileTyping"
+      "additionalOptions"
+    ]);
+
+  options = {
+
+    services.xserver.libinput = {
+      enable = mkEnableOption "libinput";
+      mouse = mkConfigForDevice "mouse";
+      touchpad = mkConfigForDevice "touchpad";
+    };
   };
 
 
@@ -212,32 +257,10 @@ in {
 
     services.udev.packages = [ pkgs.libinput.out ];
 
-    services.xserver.config =
-      ''
-        # General libinput configuration.
-        # See CONFIGURATION DETAILS section of man:libinput(4).
-        Section "InputClass"
-          Identifier "libinputConfiguration"
-          MatchDriver "libinput"
-          ${optionalString (cfg.dev != null) ''MatchDevicePath "${cfg.dev}"''}
-          Option "AccelProfile" "${cfg.accelProfile}"
-          ${optionalString (cfg.accelSpeed != null) ''Option "AccelSpeed" "${cfg.accelSpeed}"''}
-          ${optionalString (cfg.buttonMapping != null) ''Option "ButtonMapping" "${cfg.buttonMapping}"''}
-          ${optionalString (cfg.calibrationMatrix != null) ''Option "CalibrationMatrix" "${cfg.calibrationMatrix}"''}
-          ${optionalString (cfg.clickMethod != null) ''Option "ClickMethod" "${cfg.clickMethod}"''}
-          Option "LeftHanded" "${xorgBool cfg.leftHanded}"
-          Option "MiddleEmulation" "${xorgBool cfg.middleEmulation}"
-          Option "NaturalScrolling" "${xorgBool cfg.naturalScrolling}"
-          ${optionalString (cfg.scrollButton != null) ''Option "ScrollButton" "${toString cfg.scrollButton}"''}
-          Option "ScrollMethod" "${cfg.scrollMethod}"
-          Option "HorizontalScrolling" "${xorgBool cfg.horizontalScrolling}"
-          Option "SendEventsMode" "${cfg.sendEventsMode}"
-          Option "Tapping" "${xorgBool cfg.tapping}"
-          Option "TappingDragLock" "${xorgBool cfg.tappingDragLock}"
-          Option "DisableWhileTyping" "${xorgBool cfg.disableWhileTyping}"
-          ${cfg.additionalOptions}
-        EndSection
-      '';
+    services.xserver.inputClassSections = [
+      (mkX11ConfigForDevice "mouse" "Pointer")
+      (mkX11ConfigForDevice "touchpad" "Touchpad")
+    ];
 
     assertions = [
       # already present in synaptics.nix
diff --git a/nixos/modules/services/x11/redshift.nix b/nixos/modules/services/x11/redshift.nix
index 21b0b33553acc..60d80a28762b0 100644
--- a/nixos/modules/services/x11/redshift.nix
+++ b/nixos/modules/services/x11/redshift.nix
@@ -82,6 +82,15 @@ in {
       '';
     };
 
+    executable = mkOption {
+      type = types.str;
+      default = "/bin/redshift";
+      example = "/bin/redshift-gtk";
+      description = ''
+        Redshift executable to use within the package.
+      '';
+    };
+
     extraOptions = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -114,7 +123,7 @@ in {
       partOf = [ "graphical-session.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${cfg.package}/bin/redshift \
+          ${cfg.package}${cfg.executable} \
             -l ${providerString} \
             -t ${toString cfg.temperature.day}:${toString cfg.temperature.night} \
             -b ${toString cfg.brightness.day}:${toString cfg.brightness.night} \
diff --git a/nixos/modules/services/x11/terminal-server.nix b/nixos/modules/services/x11/terminal-server.nix
index 503c14c9b6245..e6b50c21a9526 100644
--- a/nixos/modules/services/x11/terminal-server.nix
+++ b/nixos/modules/services/x11/terminal-server.nix
@@ -32,7 +32,7 @@ with lib;
 
         path =
           [ pkgs.xorg.xorgserver.out pkgs.gawk pkgs.which pkgs.openssl pkgs.xorg.xauth
-            pkgs.nettools pkgs.shadow pkgs.procps pkgs.utillinux pkgs.bash
+            pkgs.nettools pkgs.shadow pkgs.procps pkgs.util-linux pkgs.bash
           ];
 
         environment.FD_GEOM = "1024x786x24";
diff --git a/nixos/modules/services/x11/window-managers/clfswm.nix b/nixos/modules/services/x11/window-managers/clfswm.nix
index 176c1f461271b..171660c53ac30 100644
--- a/nixos/modules/services/x11/window-managers/clfswm.nix
+++ b/nixos/modules/services/x11/window-managers/clfswm.nix
@@ -15,10 +15,10 @@ in
     services.xserver.windowManager.session = singleton {
       name = "clfswm";
       start = ''
-        ${pkgs.clfswm}/bin/clfswm &
+        ${pkgs.lispPackages.clfswm}/bin/clfswm &
         waitPID=$!
       '';
     };
-    environment.systemPackages = [ pkgs.clfswm ];
+    environment.systemPackages = [ pkgs.lispPackages.clfswm ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index 87702c58727a6..9ca24310e5673 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -13,6 +13,7 @@ in
     ./berry.nix
     ./bspwm.nix
     ./cwm.nix
+    ./clfswm.nix
     ./dwm.nix
     ./evilwm.nix
     ./exwm.nix
diff --git a/nixos/modules/services/x11/window-managers/evilwm.nix b/nixos/modules/services/x11/window-managers/evilwm.nix
index 6e19e3572c79d..6f1db2110f879 100644
--- a/nixos/modules/services/x11/window-managers/evilwm.nix
+++ b/nixos/modules/services/x11/window-managers/evilwm.nix
@@ -16,8 +16,8 @@ in
     services.xserver.windowManager.session = singleton {
       name = "evilwm";
       start = ''
-	${pkgs.evilwm}/bin/evilwm &
-	waitPID=$!
+        ${pkgs.evilwm}/bin/evilwm &
+        waitPID=$!
       '';
     };
     environment.systemPackages = [ pkgs.evilwm ];
diff --git a/nixos/modules/services/x11/window-managers/exwm.nix b/nixos/modules/services/x11/window-managers/exwm.nix
index dc1d957c17097..4b707d3984969 100644
--- a/nixos/modules/services/x11/window-managers/exwm.nix
+++ b/nixos/modules/services/x11/window-managers/exwm.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.xserver.windowManager.exwm;
   loadScript = pkgs.writeText "emacs-exwm-load" ''
-    (require 'exwm)
+    ${cfg.loadScript}
     ${optionalString cfg.enableDefaultConfig ''
       (require 'exwm-config)
       (exwm-config-default)
@@ -19,12 +19,26 @@ in
   options = {
     services.xserver.windowManager.exwm = {
       enable = mkEnableOption "exwm";
+      loadScript = mkOption {
+        default = "(require 'exwm)";
+        type = types.lines;
+        example = literalExample ''
+          (require 'exwm)
+          (exwm-enable)
+        '';
+        description = ''
+          Emacs lisp code to be run after loading the user's init
+          file. If enableDefaultConfig is true, this will be run
+          before loading the default config.
+        '';
+      };
       enableDefaultConfig = mkOption {
         default = true;
         type = lib.types.bool;
         description = "Enable an uncustomised exwm configuration.";
       };
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           epkgs: [
@@ -36,7 +50,7 @@ in
         description = ''
           Extra packages available to Emacs. The value must be a
           function which receives the attrset defined in
-          <varname>emacsPackages</varname> as the sole argument.
+          <varname>emacs.pkgs</varname> as the sole argument.
         '';
       };
     };
diff --git a/nixos/modules/services/x11/window-managers/herbstluftwm.nix b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
index e3ea61cb9a6be..548097a412d23 100644
--- a/nixos/modules/services/x11/window-managers/herbstluftwm.nix
+++ b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
@@ -11,6 +11,15 @@ in
     services.xserver.windowManager.herbstluftwm = {
       enable = mkEnableOption "herbstluftwm";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.herbstluftwm;
+        defaultText = "pkgs.herbstluftwm";
+        description = ''
+          Herbstluftwm package to use.
+        '';
+      };
+
       configFile = mkOption {
         default     = null;
         type        = with types; nullOr path;
@@ -31,8 +40,8 @@ in
             (cfg.configFile != null)
             ''-c "${cfg.configFile}"''
             ;
-        in "${pkgs.herbstluftwm}/bin/herbstluftwm ${configFileClause}";
+        in "${cfg.package}/bin/herbstluftwm ${configFileClause}";
     };
-    environment.systemPackages = [ pkgs.herbstluftwm ];
+    environment.systemPackages = [ cfg.package ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index dba25da8260ce..fe8ed38125114 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -4,23 +4,39 @@ with lib;
 let
   inherit (lib) mkOption mkIf optionals literalExample;
   cfg = config.services.xserver.windowManager.xmonad;
-  xmonad = pkgs.xmonad-with-packages.override {
-    ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
-    packages = self: cfg.extraPackages self ++
-                     optionals cfg.enableContribAndExtras
-                     [ self.xmonad-contrib self.xmonad-extras ];
+
+  ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
+  packages = self: cfg.extraPackages self ++
+                   optionals cfg.enableContribAndExtras
+                   [ self.xmonad-contrib self.xmonad-extras ];
+
+  xmonad-vanilla = pkgs.xmonad-with-packages.override {
+    inherit ghcWithPackages packages;
   };
-  xmonadBin = pkgs.writers.writeHaskell "xmonad" {
-    ghc = cfg.haskellPackages.ghc;
-    libraries = [ cfg.haskellPackages.xmonad ] ++
-                cfg.extraPackages cfg.haskellPackages ++
-                optionals cfg.enableContribAndExtras
-                (with cfg.haskellPackages; [ xmonad-contrib xmonad-extras ]);
-    inherit (cfg) ghcArgs;
-  } cfg.config;
-
-in
-{
+
+  xmonad-config =
+    let
+      xmonadAndPackages = self: [ self.xmonad ] ++ packages self;
+      xmonadEnv = ghcWithPackages xmonadAndPackages;
+      configured = pkgs.writers.writeHaskellBin "xmonad" {
+        ghc = cfg.haskellPackages.ghc;
+        libraries = xmonadAndPackages cfg.haskellPackages;
+        inherit (cfg) ghcArgs;
+      } cfg.config;
+    in
+      pkgs.runCommandLocal "xmonad" {
+        nativeBuildInputs = [ pkgs.makeWrapper ];
+      } ''
+        install -D ${xmonadEnv}/share/man/man1/xmonad.1.gz $out/share/man/man1/xmonad.1.gz
+        makeWrapper ${configured}/bin/xmonad $out/bin/xmonad \
+          --set NIX_GHC "${xmonadEnv}/bin/ghc" \
+          --set XMONAD_XMESSAGE "${pkgs.xorg.xmessage}/bin/xmessage"
+      '';
+
+  xmonad = if (cfg.config != null) then xmonad-config else xmonad-vanilla;
+in {
+  meta.maintainers = with maintainers; [ lassulus xaverdh ivanbrennan ];
+
   options = {
     services.xserver.windowManager.xmonad = {
       enable = mkEnableOption "xmonad";
@@ -37,6 +53,7 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         defaultText = "self: []";
         example = literalExample ''
@@ -62,19 +79,56 @@ in
         default = null;
         type = with lib.types; nullOr (either path str);
         description = ''
-          Configuration from which XMonad gets compiled. If no value
-          is specified, the xmonad config from $HOME/.xmonad is taken.
-          If you use xmonad --recompile, $HOME/.xmonad will be taken as
-          the configuration, but on the next restart of display-manager
-          this config will be reapplied.
+          Configuration from which XMonad gets compiled. If no value is
+          specified, a vanilla xmonad binary is put in PATH, which will
+          attempt to recompile and exec your xmonad config from $HOME/.xmonad.
+          This setup is then analogous to other (non-NixOS) linux distributions.
+
+          If you do set this option, you likely want to use "launch" as your
+          entry point for xmonad (as in the example), to avoid xmonad's
+          recompilation logic on startup. Doing so will render the default
+          "mod+q" restart key binding dysfunctional though, because that attempts
+          to call your binary with the "--restart" command line option, unless
+          you implement that yourself. You way mant to bind "mod+q" to
+          <literal>(restart "xmonad" True)</literal> instead, which will just restart
+          xmonad from PATH. This allows e.g. switching to the new xmonad binary
+          after rebuilding your system with nixos-rebuild.
+
+          If you actually want to run xmonad with a config specified here, but
+          also be able to recompile and restart it from a copy of that source in
+          $HOME/.xmonad on the fly, you will have to implement that yourself
+          using something like "compileRestart" from the example.
+          This should allow you to switch at will between the local xmonad and
+          the one NixOS puts in your PATH.
         '';
         example = ''
           import XMonad
+          import XMonad.Util.EZConfig (additionalKeys)
+          import Control.Monad (when)
+          import Text.Printf (printf)
+          import System.Posix.Process (executeFile)
+          import System.Info (arch,os)
+          import System.Environment (getArgs)
+          import System.FilePath ((</>))
+
+          compiledConfig = printf "xmonad-%s-%s" arch os
+
+          compileRestart resume =
+            whenX (recompile True) $
+              when resume writeStateToFile
+                *> catchIO
+                  ( do
+                      dir <- getXMonadDataDir
+                      args <- getArgs
+                      executeFile (dir </> compiledConfig) False args Nothing
+                  )
 
           main = launch defaultConfig
-                 { modMask = mod4Mask -- Use Super instead of Alt
-                 , terminal = "urxvt"
-                 }
+              { modMask = mod4Mask -- Use Super instead of Alt
+              , terminal = "urxvt" }
+              `additionalKeys`
+              [ ( (mod4Mask,xK_r), compileRestart True)
+              , ( (mod4Mask,xK_q), restart "xmonad" True ) ]
         '';
       };
 
@@ -101,10 +155,8 @@ in
     services.xserver.windowManager = {
       session = [{
         name = "xmonad";
-        start = let
-          xmonadCommand = if (cfg.config != null) then xmonadBin else "${xmonad}/bin/xmonad";
-        in ''
-           systemd-cat -t xmonad -- ${xmonadCommand} ${lib.escapeShellArgs cfg.xmonadCliArgs} &
+        start = ''
+           systemd-cat -t xmonad -- ${xmonad}/bin/xmonad ${lib.escapeShellArgs cfg.xmonadCliArgs} &
            waitPID=$!
         '';
       }];
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 8223c1f1e6753..8858559d8f27d 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -152,6 +152,9 @@ in
       ./desktop-managers/default.nix
       (mkRemovedOptionModule [ "services" "xserver" "startGnuPGAgent" ]
         "See the 16.09 release notes for more information.")
+      (mkRemovedOptionModule
+        [ "services" "xserver" "startDbusSession" ]
+        "The user D-Bus session is now always socket activated and this option can safely be removed.")
       (mkRemovedOptionModule ["services" "xserver" "useXFS" ]
         "Use services.xserver.fontPath instead of useXFS")
     ];
@@ -299,14 +302,6 @@ in
         description = "DPI resolution to use for X server.";
       };
 
-      startDbusSession = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to start a new DBus session when you log in with dbus-launch.
-        '';
-      };
-
       updateDbusEnvironment = mkOption {
         type = types.bool;
         default = false;
@@ -446,6 +441,7 @@ in
 
       serverFlagsSection = mkOption {
         default = "";
+        type = types.lines;
         example =
           ''
           Option "BlankTime" "0"
@@ -523,6 +519,19 @@ in
         '';
       };
 
+      logFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/dev/null";
+        example = "/var/log/Xorg.0.log";
+        description = ''
+          Controls the file Xorg logs to.
+
+          The default of <literal>/dev/null</literal> is set so that systemd services (like <literal>displayManagers</literal>) only log to the journal and don't create their own log files.
+
+          Setting this to <literal>null</literal> will not pass the <literal>-logfile</literal> argument to Xorg which allows it to log to its default logfile locations instead (see <literal>man Xorg</literal>). You probably only want this behaviour when running Xorg manually (e.g. via <literal>startx</literal>).
+        '';
+      };
+
       verbose = mkOption {
         type = types.nullOr types.int;
         default = 3;
@@ -641,7 +650,7 @@ in
         xorg.xprop
         xorg.xauth
         pkgs.xterm
-        pkgs.xdg_utils
+        pkgs.xdg-utils
         xorg.xf86inputevdev.out # get evdev.4 man page
       ]
       ++ optional (elem "virtualbox" cfg.videoDrivers) xorg.xrefresh;
@@ -683,25 +692,24 @@ in
 
         script = "${cfg.displayManager.job.execCmd}";
 
+        # Stop restarting if the display manager stops (crashes) 2 times
+        # in one minute. Starting X typically takes 3-4s.
+        startLimitIntervalSec = 30;
+        startLimitBurst = 3;
         serviceConfig = {
           Restart = "always";
           RestartSec = "200ms";
           SyslogIdentifier = "display-manager";
-          # Stop restarting if the display manager stops (crashes) 2 times
-          # in one minute. Starting X typically takes 3-4s.
-          StartLimitInterval = "30s";
-          StartLimitBurst = "3";
         };
       };
 
     services.xserver.displayManager.xserverArgs =
       [ "-config ${configFile}"
         "-xkbdir" "${cfg.xkbDir}"
-        # Log at the default verbosity level to stderr rather than /var/log/X.*.log.
-         "-logfile" "/dev/null"
       ] ++ optional (cfg.display != null) ":${toString cfg.display}"
         ++ optional (cfg.tty     != null) "vt${toString cfg.tty}"
         ++ optional (cfg.dpi     != null) "-dpi ${toString cfg.dpi}"
+        ++ optional (cfg.logFile != null) "-logfile ${toString cfg.logFile}"
         ++ optional (cfg.verbose != null) "-verbose ${toString cfg.verbose}"
         ++ optional (!cfg.enableTCP) "-nolisten tcp"
         ++ optional (cfg.autoRepeatDelay != null) "-ardelay ${toString cfg.autoRepeatDelay}"
@@ -716,7 +724,7 @@ in
 
     system.extraDependencies = singleton (pkgs.runCommand "xkb-validated" {
       inherit (cfg) xkbModel layout xkbVariant xkbOptions;
-      nativeBuildInputs = [ pkgs.xkbvalidate ];
+      nativeBuildInputs = with pkgs.buildPackages; [ xkbvalidate ];
       preferLocalBuild = true;
     } ''
       xkbvalidate "$xkbModel" "$layout" "$xkbVariant" "$xkbOptions"
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
index ddfd1af4a3190..3a6930314b1af 100644
--- a/nixos/modules/system/activation/activation-script.nix
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -25,9 +25,23 @@ let
       stdenv.cc.libc # nscd in update-users-groups.pl
       shadow
       nettools # needed for hostname
-      utillinux # needed for mount and mountpoint
+      util-linux # needed for mount and mountpoint
     ];
 
+  scriptType = with types;
+    let scriptOptions =
+      { deps = mkOption
+          { type = types.listOf types.str;
+            default = [ ];
+            description = "List of dependencies. The script will run after these.";
+          };
+        text = mkOption
+          { type = types.lines;
+            description = "The content of the script.";
+          };
+      };
+    in either str (submodule { options = scriptOptions; });
+
 in
 
 {
@@ -40,16 +54,14 @@ in
       default = {};
 
       example = literalExample ''
-        { stdio = {
-            text = '''
-              # Needed by some programs.
-              ln -sfn /proc/self/fd /dev/fd
-              ln -sfn /proc/self/fd/0 /dev/stdin
-              ln -sfn /proc/self/fd/1 /dev/stdout
-              ln -sfn /proc/self/fd/2 /dev/stderr
-            ''';
-            deps = [];
-          };
+        { stdio.text =
+          '''
+            # Needed by some programs.
+            ln -sfn /proc/self/fd /dev/fd
+            ln -sfn /proc/self/fd/0 /dev/stdin
+            ln -sfn /proc/self/fd/1 /dev/stdout
+            ln -sfn /proc/self/fd/2 /dev/stderr
+          ''';
         }
       '';
 
@@ -62,7 +74,7 @@ in
         idempotent and fast.
       '';
 
-      type = types.attrsOf types.unspecified; # FIXME
+      type = types.attrsOf scriptType;
 
       apply = set: {
         script =
@@ -125,7 +137,7 @@ in
         idempotent and fast.
       '';
 
-      type = types.attrsOf types.unspecified;
+      type = with types; attrsOf scriptType;
 
       apply = set: {
         script = ''
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 2724d9f9cb6f1..b0f77ca3fb8d3 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -97,10 +97,11 @@ let
     allowSubstitutes = false;
     buildCommand = systemBuilder;
 
-    inherit (pkgs) utillinux coreutils;
+    inherit (pkgs) coreutils;
     systemd = config.systemd.package;
     shell = "${pkgs.bash}/bin/sh";
     su = "${pkgs.shadow.su}/bin/su";
+    utillinux = pkgs.util-linux;
 
     kernelParams = config.boot.kernelParams;
     installBootLoader =
@@ -189,7 +190,7 @@ in
 
     system.boot.loader.kernelFile = mkOption {
       internal = true;
-      default = pkgs.stdenv.hostPlatform.platform.kernelTarget;
+      default = pkgs.stdenv.hostPlatform.linux-kernel.target;
       type = types.str;
       description = ''
         Name of the kernel file to be passed to the bootloader.
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index 9eeae0c3ef447..cbdf581d73a7f 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -20,8 +20,14 @@ let
                  optionalString fixBinary "F";
   in ":${name}:${type}:${offset'}:${magicOrExtension}:${mask'}:${interpreter}:${flags}";
 
-  activationSnippet = name: { interpreter, ... }:
-    "ln -sf ${interpreter} /run/binfmt/${name}";
+  activationSnippet = name: { interpreter, ... }: ''
+    rm -f /run/binfmt/${name}
+    cat > /run/binfmt/${name} << 'EOF'
+    #!${pkgs.bash}/bin/sh
+    exec -- ${interpreter} "$@"
+    EOF
+    chmod +x /run/binfmt/${name}
+  '';
 
   getEmulator = system: (lib.systems.elaborate { inherit system; }).emulator pkgs;
 
@@ -260,7 +266,7 @@ in {
       extra-platforms = ${toString (cfg.emulatedSystems ++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 "i686-linux")}
     '';
     nix.sandboxPaths = lib.mkIf (cfg.emulatedSystems != [])
-      ([ "/run/binfmt" ] ++ (map (system: dirOf (dirOf (getEmulator system))) cfg.emulatedSystems));
+      ([ "/run/binfmt" "${pkgs.bash}" ] ++ (map (system: dirOf (dirOf (getEmulator system))) cfg.emulatedSystems));
 
     environment.etc."binfmt.d/nixos.conf".source = builtins.toFile "binfmt_nixos.conf"
       (lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine config.boot.binfmt.registrations));
diff --git a/nixos/modules/system/boot/grow-partition.nix b/nixos/modules/system/boot/grow-partition.nix
index 71a86c74772e7..87c981b24cec9 100644
--- a/nixos/modules/system/boot/grow-partition.nix
+++ b/nixos/modules/system/boot/grow-partition.nix
@@ -20,10 +20,10 @@ with lib;
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs ${pkgs.gawk}/bin/gawk
       copy_bin_and_libs ${pkgs.gnused}/bin/sed
-      copy_bin_and_libs ${pkgs.utillinux}/sbin/sfdisk
-      copy_bin_and_libs ${pkgs.utillinux}/sbin/lsblk
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/sfdisk
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/lsblk
 
-      substitute "${pkgs.cloud-utils}/bin/.growpart-wrapped" "$out/bin/growpart" \
+      substitute "${pkgs.cloud-utils.guest}/bin/.growpart-wrapped" "$out/bin/growpart" \
         --replace "${pkgs.bash}/bin/sh" "/bin/sh" \
         --replace "awk" "gawk" \
         --replace "sed" "gnused"
diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix
index ec794d6eb014d..2a7417ed37153 100644
--- a/nixos/modules/system/boot/initrd-network.nix
+++ b/nixos/modules/system/boot/initrd-network.nix
@@ -32,8 +32,8 @@ let
         fi
         if [ -n "$dns" ]; then
           rm -f /etc/resolv.conf
-          for i in $dns; do
-            echo "nameserver $dns" >> /etc/resolv.conf
+          for server in $dns; do
+            echo "nameserver $server" >> /etc/resolv.conf
           done
         fi
       fi
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
index f7ef261037090..00ac83a189724 100644
--- a/nixos/modules/system/boot/initrd-ssh.nix
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -159,9 +159,14 @@ in
 
     boot.initrd.extraUtilsCommandsTest = ''
       # sshd requires a host key to check config, so we pass in the test's
+      tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
+      cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
+      # keys from Nix store are world-readable, which sshd doesn't like
+      chmod 600 "$tmpkey"
       echo -n ${escapeShellArg sshdConfig} |
         $out/bin/sshd -t -f /dev/stdin \
-        -h ${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}
+        -h "$tmpkey"
+      rm "$tmpkey"
     '';
 
     boot.initrd.network.postCommands = ''
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
index 43871f439f7f3..363d8e47a0ff1 100644
--- a/nixos/modules/system/boot/kernel.nix
+++ b/nixos/modules/system/boot/kernel.nix
@@ -156,6 +156,16 @@ in
       description = "List of modules that are always loaded by the initrd.";
     };
 
+    boot.initrd.includeDefaultModules = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        This option, if set, adds a collection of default kernel modules
+        to <option>boot.initrd.availableKernelModules</option> and
+        <option>boot.initrd.kernelModules</option>.
+      '';
+    };
+
     system.modulesTree = mkOption {
       type = types.listOf types.path;
       internal = true;
@@ -195,7 +205,8 @@ in
   config = mkMerge
     [ (mkIf config.boot.initrd.enable {
         boot.initrd.availableKernelModules =
-          [ # Note: most of these (especially the SATA/PATA modules)
+          optionals config.boot.initrd.includeDefaultModules ([
+            # Note: most of these (especially the SATA/PATA modules)
             # shouldn't be included by default since nixos-generate-config
             # detects them, but I'm keeping them for now for backwards
             # compatibility.
@@ -227,7 +238,7 @@ in
             "xhci_pci"
             "usbhid"
             "hid_generic" "hid_lenovo" "hid_apple" "hid_roccat"
-            "hid_logitech_hidpp" "hid_logitech_dj"
+            "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft"
 
           ] ++ optionals (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) [
             # Misc. x86 keyboard stuff.
@@ -235,10 +246,11 @@ in
 
             # x86 RTC needed by the stage 2 init script.
             "rtc_cmos"
-          ];
+          ]);
 
         boot.initrd.kernelModules =
-          [ # For LVM.
+          optionals config.boot.initrd.includeDefaultModules [
+            # For LVM.
             "dm_mod"
           ];
       })
diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix
index 2d27611946e2c..1437ab3877009 100644
--- a/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix
+++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix
@@ -12,9 +12,6 @@ let
     inherit (config.boot.loader.generationsDir) copyKernels;
   };
 
-  # Temporary check, for nixos to cope both with nixpkgs stdenv-updates and trunk
-  inherit (pkgs.stdenv.hostPlatform) platform;
-
 in
 
 {
@@ -59,7 +56,7 @@ in
 
     system.build.installBootLoader = generationsDirBuilder;
     system.boot.loader.id = "generationsDir";
-    system.boot.loader.kernelFile = platform.kernelTarget;
+    system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
 
   };
 }
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 854684b87face..5ffffb95edb1b 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
@@ -109,7 +109,7 @@ addEntry() {
             exit 1
         fi
     fi
-    echo "  APPEND systemConfig=$path init=$path/init $extraParams"
+    echo "  APPEND init=$path/init $extraParams"
 }
 
 tmpFile="$target/extlinux/extlinux.conf.tmp.$$"
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 20e39628eabbc..289c2b199862e 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -66,7 +66,7 @@ let
         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 ]
+        [ coreutils gnused gnugrep findutils diffutils btrfs-progs util-linux mdadm ]
         ++ optional (cfg.efiSupport && (cfg.version == 2)) efibootmgr
         ++ optionals cfg.useOSProber [ busybox os-prober ]);
       font = if cfg.font == null then ""
@@ -327,6 +327,26 @@ in
         '';
       };
 
+      extraInstallCommands = mkOption {
+        default = "";
+        example = literalExample ''
+          # the example below generates detached signatures that GRUB can verify
+          # https://www.gnu.org/software/grub/manual/grub/grub.html#Using-digital-signatures
+          ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -name '*.sig' -delete
+          old_gpg_home=$GNUPGHOME
+          export GNUPGHOME="$(mktemp -d)"
+          ''${pkgs.gnupg}/bin/gpg --import ''${priv_key} > /dev/null 2>&1
+          ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -exec ''${pkgs.gnupg}/bin/gpg --detach-sign "{}" \; > /dev/null 2>&1
+          rm -rf $GNUPGHOME
+          export GNUPGHOME=$old_gpg_home
+        '';
+        type = types.lines;
+        description = ''
+          Additional shell commands inserted in the bootloader installer
+          script after generating menu entries.
+        '';
+      };
+
       extraPerEntryConfig = mkOption {
         default = "";
         example = "root (hd0)";
@@ -705,7 +725,7 @@ in
         let
           install-grub-pl = pkgs.substituteAll {
             src = ./install-grub.pl;
-            inherit (pkgs) utillinux;
+            utillinux = pkgs.util-linux;
             btrfsprogs = pkgs.btrfs-progs;
           };
         in pkgs.writeScript "install-grub.sh" (''
@@ -715,7 +735,7 @@ in
         ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
       '' + flip concatMapStrings cfg.mirroredBoots (args: ''
         ${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
-      ''));
+      '') + cfg.extraInstallCommands);
 
       system.build.grub = grub;
 
@@ -741,7 +761,7 @@ in
             + "'boot.loader.grub.mirroredBoots' to make the system bootable.";
         }
         {
-          assertion = cfg.efiSupport || all (c: c < 2) (mapAttrsToList (_: c: c) bootDeviceCounters);
+          assertion = cfg.efiSupport || all (c: c < 2) (mapAttrsToList (n: c: if n == "nodev" then 0 else c) bootDeviceCounters);
           message = "You cannot have duplicated devices in mirroredBoots";
         }
         {
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index 59f5638044fe9..e0167654748e1 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -102,10 +102,10 @@ if (stat($bootPath)->dev != stat("/nix/store")->dev) {
 
 # Discover information about the location of the bootPath
 struct(Fs => {
-    device => '$',
-    type => '$',
-    mount => '$',
-});
+        device => '$',
+        type => '$',
+        mount => '$',
+    });
 sub PathInMount {
     my ($path, $mount) = @_;
     my @splitMount = split /\//, $mount;
@@ -154,16 +154,16 @@ sub GetFs {
     return $bestFs;
 }
 struct (Grub => {
-    path => '$',
-    search => '$',
-});
+        path => '$',
+        search => '$',
+    });
 my $driveid = 1;
 sub GrubFs {
     my ($dir) = @_;
     my $fs = GetFs($dir);
     my $path = substr($dir, length($fs->mount));
     if (substr($path, 0, 1) ne "/") {
-      $path = "/$path";
+        $path = "/$path";
     }
     my $search = "";
 
@@ -251,8 +251,8 @@ my $conf .= "# Automatically generated.  DO NOT EDIT THIS FILE!\n";
 
 if ($grubVersion == 1) {
     $conf .= "
-        default $defaultEntry
-        timeout $timeout
+    default $defaultEntry
+    timeout $timeout
     ";
     if ($splashImage) {
         copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
@@ -302,51 +302,51 @@ else {
 
     if ($copyKernels == 0) {
         $conf .= "
-            " . $grubStore->search;
+        " . $grubStore->search;
     }
     # FIXME: should use grub-mkconfig.
     $conf .= "
-        " . $grubBoot->search . "
-        if [ -s \$prefix/grubenv ]; then
-          load_env
-        fi
-
-        # ‘grub-reboot’ sets a one-time saved entry, which we process here and
-        # then delete.
-        if [ \"\${next_entry}\" ]; then
-          set default=\"\${next_entry}\"
-          set next_entry=
-          save_env next_entry
-          set timeout=1
-        else
-          set default=$defaultEntry
-          set timeout=$timeout
-        fi
-
-        # Setup the graphics stack for bios and efi systems
-        if [ \"\${grub_platform}\" = \"efi\" ]; then
-          insmod efi_gop
-          insmod efi_uga
-        else
-          insmod vbe
-        fi
+    " . $grubBoot->search . "
+    if [ -s \$prefix/grubenv ]; then
+    load_env
+    fi
+
+    # ‘grub-reboot’ sets a one-time saved entry, which we process here and
+    # then delete.
+    if [ \"\${next_entry}\" ]; then
+    set default=\"\${next_entry}\"
+    set next_entry=
+    save_env next_entry
+    set timeout=1
+    else
+    set default=$defaultEntry
+    set timeout=$timeout
+    fi
+
+    # Setup the graphics stack for bios and efi systems
+    if [ \"\${grub_platform}\" = \"efi\" ]; then
+    insmod efi_gop
+    insmod efi_uga
+    else
+    insmod vbe
+    fi
     ";
 
     if ($font) {
         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
-              insmod gfxterm
-              if [ \"\${grub_platform}\" = \"efi\" ]; then
-                set gfxmode=$gfxmodeEfi
-                set gfxpayload=$gfxpayloadEfi
-              else
-                set gfxmode=$gfxmodeBios
-                set gfxpayload=$gfxpayloadBios
-              fi
-              terminal_output gfxterm
-            fi
+        insmod font
+        if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
+        insmod gfxterm
+        if [ \"\${grub_platform}\" = \"efi\" ]; then
+        set gfxmode=$gfxmodeEfi
+        set gfxpayload=$gfxpayloadEfi
+        else
+        set gfxmode=$gfxmodeBios
+        set gfxpayload=$gfxpayloadBios
+        fi
+        terminal_output gfxterm
+        fi
         ";
     }
     if ($splashImage) {
@@ -356,21 +356,21 @@ else {
         if ($suffix eq ".jpg") {
             $suffix = ".jpeg";
         }
-		if ($backgroundColor) {
-			$conf .= "
-		    background_color '$backgroundColor'
-		    ";
-		}
+        if ($backgroundColor) {
+            $conf .= "
+            background_color '$backgroundColor'
+            ";
+        }
         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
-              set color_normal=white/black
-              set color_highlight=black/white
-            else
-              set menu_color_normal=cyan/blue
-              set menu_color_highlight=white/blue
-            fi
+        insmod " . substr($suffix, 1) . "
+        if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
+        set color_normal=white/black
+        set color_highlight=black/white
+        else
+        set menu_color_normal=cyan/blue
+        set menu_color_highlight=white/blue
+        fi
         ";
     }
 
@@ -380,21 +380,21 @@ else {
         # 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 );
-     }
+        # 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";
@@ -433,25 +433,25 @@ sub addEntry {
 
     # 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 $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;
@@ -459,9 +459,8 @@ sub addEntry {
     # FIXME: $confName
 
     my $kernelParams =
-        "systemConfig=" . Cwd::abs_path($path) . " " .
-        "init=" . Cwd::abs_path("$path/init") . " " .
-        readFile("$path/kernel-params");
+    "init=" . Cwd::abs_path("$path/init") . " " .
+    readFile("$path/kernel-params");
     my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
 
     if ($grubVersion == 1) {
@@ -503,9 +502,9 @@ foreach my $link (@links) {
 
     my $date = strftime("%F", localtime(lstat($link)->mtime));
     my $version =
-        -e "$link/nixos-version"
-        ? readFile("$link/nixos-version")
-        : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
+    -e "$link/nixos-version"
+    ? readFile("$link/nixos-version")
+    : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
 
     if ($cfgName) {
         $entryName = $cfgName;
@@ -530,8 +529,8 @@ sub addProfile {
     sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
 
     my @links = sort
-        { nrFromGen($b) <=> nrFromGen($a) }
-        (glob "$profile-*-link");
+    { nrFromGen($b) <=> nrFromGen($a) }
+    (glob "$profile-*-link");
 
     my $curEntry = 0;
     foreach my $link (@links) {
@@ -542,9 +541,9 @@ sub addProfile {
         }
         my $date = strftime("%F", localtime(lstat($link)->mtime));
         my $version =
-            -e "$link/nixos-version"
-            ? readFile("$link/nixos-version")
-            : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
+        -e "$link/nixos-version"
+        ? readFile("$link/nixos-version")
+        : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
         addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link);
     }
 
@@ -566,7 +565,7 @@ $extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g;
 
 # Run extraPrepareConfig in sh
 if ($extraPrepareConfig ne "") {
-  system((get("shell"), "-c", $extraPrepareConfig));
+    system((get("shell"), "-c", $extraPrepareConfig));
 }
 
 # write the GRUB config.
@@ -627,13 +626,13 @@ foreach my $fn (glob "$bootPath/kernels/*") {
 #
 
 struct(GrubState => {
-    name => '$',
-    version => '$',
-    efi => '$',
-    devices => '$',
-    efiMountPoint => '$',
-    extraGrubInstallArgs => '@',
-});
+        name => '$',
+        version => '$',
+        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 {
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 2a1ec479fea04..bd3fc64999da5 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
@@ -49,7 +49,6 @@ addEntry() {
       echo "#!/bin/sh"
       echo "# $name"
       echo "# created by init-script-builder.sh"
-      echo "export systemConfig=$(readlink -f $path)"
       echo "exec $stage2"
     )"
 
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix
index 7eb52e3d021ff..64e106036abd1 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix
@@ -1,10 +1,9 @@
-{ pkgs, configTxt }:
+{ pkgs, configTxt, firmware ? pkgs.raspberrypifw }:
 
 pkgs.substituteAll {
   src = ./raspberrypi-builder.sh;
   isExecutable = true;
   inherit (pkgs) bash;
   path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
-  firmware = pkgs.raspberrypifw;
-  inherit configTxt;
+  inherit firmware configTxt;
 }
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
index 337afe9ef628d..1023361f0b1f6 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -5,8 +5,6 @@ with lib;
 let
   cfg = config.boot.loader.raspberryPi;
 
-  inherit (pkgs.stdenv.hostPlatform) platform;
-
   builderUboot = import ./uboot-builder.nix { inherit pkgs configTxt; inherit (cfg) version; };
   builderGeneric = import ./raspberrypi-builder.nix { inherit pkgs configTxt; };
 
@@ -20,7 +18,7 @@ let
   timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
 
   isAarch64 = pkgs.stdenv.hostPlatform.isAarch64;
-  optional = pkgs.stdenv.lib.optionalString;
+  optional = pkgs.lib.optionalString;
 
   configTxt =
     pkgs.writeText "config.txt" (''
@@ -60,8 +58,7 @@ in
       version = mkOption {
         default = 2;
         type = types.enum [ 0 1 2 3 4 ];
-        description = ''
-        '';
+        description = "";
       };
 
       uboot = {
@@ -103,6 +100,6 @@ in
 
     system.build.installBootLoader = builder;
     system.boot.loader.id = "raspberrypi";
-    system.boot.loader.kernelFile = platform.kernelTarget;
+    system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
   };
 }
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 65c7b825f8559..6bee900c68304 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
@@ -101,7 +101,7 @@ def write_entry(profile, generation, machine_id):
         entry_file = "@efiSysMountPoint@/loader/entries/nixos-generation-%d.conf" % (generation)
     generation_dir = os.readlink(system_dir(profile, generation))
     tmp_path = "%s.tmp" % (entry_file)
-    kernel_params = "systemConfig=%s init=%s/init " % (generation_dir, generation_dir)
+    kernel_params = "init=%s/init " % generation_dir
 
     with open("%s/kernel-params" % (generation_dir)) as params_file:
         kernel_params = kernel_params + params_file.read()
@@ -200,9 +200,7 @@ def main():
     else:
         # Update bootloader to latest if needed
         systemd_version = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[1]
-        # Ideally this should use check_output as well, but as a temporary
-        # work-around for #97433 we ignore any errors.
-        sdboot_status = subprocess.run(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True, stdout=subprocess.PIPE).stdout
+        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+)\)$",
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 88190e8200b16..fa14d86e253df 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -56,7 +56,7 @@ let
 
         ykinfo -v 1>/dev/null 2>&1
         if [ $? != 0 ]; then
-            echo -n "Waiting $secs seconds for Yubikey to appear..."
+            echo -n "Waiting $secs seconds for YubiKey to appear..."
             local success=false
             for try in $(seq $secs); do
                 echo -n .
@@ -118,7 +118,7 @@ let
     # Cryptsetup locking directory
     mkdir -p /run/cryptsetup
 
-    # For Yubikey salt storage
+    # For YubiKey salt storage
     mkdir -p /crypt-storage
 
     ${optionalString luks.gpgSupport ''
@@ -218,7 +218,7 @@ let
     }
 
     ${optionalString (luks.yubikeySupport && (yubikey != null)) ''
-    # Yubikey
+    # YubiKey
     rbtohex() {
         ( od -An -vtx1 | tr -d ' \n' )
     }
@@ -244,7 +244,7 @@ let
         local new_k_luks
 
         mount -t ${yubikey.storage.fsType} ${yubikey.storage.device} /crypt-storage || \
-          die "Failed to mount Yubikey salt storage device"
+          die "Failed to mount YubiKey salt storage device"
 
         salt="$(cat /crypt-storage${yubikey.storage.path} | sed -n 1p | tr -d '\n')"
         iterations="$(cat /crypt-storage${yubikey.storage.path} | sed -n 2p | tr -d '\n')"
@@ -254,8 +254,27 @@ let
         for try in $(seq 3); do
             ${optionalString yubikey.twoFactor ''
             echo -n "Enter two-factor passphrase: "
-            read -r k_user
-            echo
+            k_user=
+            while true; do
+                if [ -e /crypt-ramfs/passphrase ]; then
+                    echo "reused"
+                    k_user=$(cat /crypt-ramfs/passphrase)
+                    break
+                else
+                    # Try reading it from /dev/console with a timeout
+                    IFS= read -t 1 -r k_user
+                    if [ -n "$k_user" ]; then
+                       ${if luks.reusePassphrases then ''
+                         # Remember it for the next device
+                         echo -n "$k_user" > /crypt-ramfs/passphrase
+                       '' else ''
+                         # Don't save it to ramfs. We are very paranoid
+                       ''}
+                       echo
+                       break
+                    fi
+                fi
+            done
             ''}
 
             if [ ! -z "$k_user" ]; then
@@ -268,6 +287,11 @@ let
 
             if [ $? == 0 ]; then
                 opened=true
+                ${if luks.reusePassphrases then ''
+                  # We don't rm here because we might reuse it for the next device
+                '' else ''
+                  rm -f /crypt-ramfs/passphrase
+                ''}
                 break
             else
                 opened=false
@@ -317,7 +341,7 @@ let
         if wait_yubikey ${toString yubikey.gracePeriod}; then
             do_open_yubikey
         else
-            echo "No yubikey found, falling back to non-yubikey open procedure"
+            echo "No YubiKey found, falling back to non-YubiKey open procedure"
             open_normally
         fi
     }
@@ -404,7 +428,7 @@ let
           echo "Please move your mouse to create needed randomness."
         ''}
           echo "Waiting for your FIDO2 device..."
-          fido2luks -i open ${device} ${name} ${fido2.credential} --await-dev ${toString fido2.gracePeriod} --salt string:$passphrase
+          fido2luks open ${device} ${name} ${fido2.credential} --await-dev ${toString fido2.gracePeriod} --salt string:$passphrase
         if [ $? -ne 0 ]; then
           echo "No FIDO2 key found, falling back to normal open procedure"
           open_normally
@@ -665,8 +689,8 @@ in
           yubikey = mkOption {
             default = null;
             description = ''
-              The options to use for this LUKS device in Yubikey-PBA.
-              If null (the default), Yubikey-PBA will be disabled for this device.
+              The options to use for this LUKS device in YubiKey-PBA.
+              If null (the default), YubiKey-PBA will be disabled for this device.
             '';
 
             type = with types; nullOr (submodule {
@@ -674,13 +698,13 @@ in
                 twoFactor = mkOption {
                   default = true;
                   type = types.bool;
-                  description = "Whether to use a passphrase and a Yubikey (true), or only a Yubikey (false).";
+                  description = "Whether to use a passphrase and a YubiKey (true), or only a YubiKey (false).";
                 };
 
                 slot = mkOption {
                   default = 2;
                   type = types.int;
-                  description = "Which slot on the Yubikey to challenge.";
+                  description = "Which slot on the YubiKey to challenge.";
                 };
 
                 saltLength = mkOption {
@@ -704,7 +728,7 @@ in
                 gracePeriod = mkOption {
                   default = 10;
                   type = types.int;
-                  description = "Time in seconds to wait for the Yubikey.";
+                  description = "Time in seconds to wait for the YubiKey.";
                 };
 
                 /* TODO: Add to the documentation of the current module:
@@ -779,9 +803,9 @@ in
       default = false;
       type = types.bool;
       description = ''
-            Enables support for authenticating with a Yubikey on LUKS devices.
+            Enables support for authenticating with a YubiKey on LUKS devices.
             See the NixOS wiki for information on how to properly setup a LUKS device
-            and a Yubikey to work with this feature.
+            and a YubiKey to work with this feature.
           '';
     };
 
@@ -799,7 +823,7 @@ in
 
     assertions =
       [ { assertion = !(luks.gpgSupport && luks.yubikeySupport);
-          message = "Yubikey and GPG Card may not be used at the same time.";
+          message = "YubiKey and GPG Card may not be used at the same time.";
         }
 
         { assertion = !(luks.gpgSupport && luks.fido2Support);
@@ -807,7 +831,7 @@ in
         }
 
         { assertion = !(luks.fido2Support && luks.yubikeySupport);
-          message = "FIDO2 and Yubikey may not be used at the same time.";
+          message = "FIDO2 and YubiKey may not be used at the same time.";
         }
       ];
 
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 47689b2a4700a..914d3e62eb423 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -648,11 +648,13 @@ let
           "RapidCommit"
           "ForceDHCPv6PDOtherInformation"
           "PrefixDelegationHint"
+          "RouteMetric"
         ])
         (assertValueOneOf "UseDNS" boolValues)
         (assertValueOneOf "UseNTP" boolValues)
         (assertValueOneOf "RapidCommit" boolValues)
         (assertValueOneOf "ForceDHCPv6PDOtherInformation" boolValues)
+        (assertInt "RouteMetric")
       ];
 
       sectionDHCPServer = checkUnitConfig "DHCPServer" [
@@ -1551,9 +1553,6 @@ in
         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" ];
-        after = [ "systemd-udev-settle.service" ];
       };
 
       systemd.services.systemd-networkd-wait-online = {
diff --git a/nixos/modules/system/boot/pbkdf2-sha512.c b/nixos/modules/system/boot/pbkdf2-sha512.c
index b40c383ac0233..67e989957ba6c 100644
--- a/nixos/modules/system/boot/pbkdf2-sha512.c
+++ b/nixos/modules/system/boot/pbkdf2-sha512.c
@@ -35,4 +35,4 @@ int main(int argc, char** argv)
 	fwrite(key, 1, key_length, stdout);
 
 	return 0;
-}
\ No newline at end of file
+}
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 55e5b07ed615d..662576888fc20 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -9,7 +9,7 @@ let
 
   cfg = config.boot.plymouth;
 
-  nixosBreezePlymouth = pkgs.breeze-plymouth.override {
+  nixosBreezePlymouth = pkgs.plasma5Packages.breeze-plymouth.override {
     logoFile = cfg.logo;
     logoName = "nixos";
     osName = "NixOS";
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index b024f9cf5ee93..7fe8f4dfb7e3a 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, pkgs, lib, ... }:
 
 with lib;
 let
@@ -136,7 +136,7 @@ in
       }
     ];
 
-    users.users.resolved.group = "systemd-resolve";
+    users.users.systemd-resolve.group = "systemd-resolve";
 
     # add resolve to nss hosts database if enabled and nscd enabled
     # system.nssModules is configured in nixos/modules/system/boot/systemd.nix
@@ -150,6 +150,9 @@ in
       wantedBy = [ "multi-user.target" ];
       aliases = [ "dbus-org.freedesktop.resolve1.service" ];
       restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
+      # Upstream bug: https://github.com/systemd/systemd/issues/18078
+      # systemd-resolved without libidn2 is broken
+      environment.LD_LIBRARY_PATH = "${lib.getLib pkgs.libidn2}/lib";
     };
 
     environment.etc = {
diff --git a/nixos/modules/system/boot/shutdown.nix b/nixos/modules/system/boot/shutdown.nix
index 11041066e07c1..8cda7b3aabe8c 100644
--- a/nixos/modules/system/boot/shutdown.nix
+++ b/nixos/modules/system/boot/shutdown.nix
@@ -18,7 +18,7 @@ with lib;
 
       serviceConfig = {
         Type = "oneshot";
-        ExecStart = "${pkgs.utillinux}/sbin/hwclock --systohc ${if config.time.hardwareClockInLocalTime then "--localtime" else "--utc"}";
+        ExecStart = "${pkgs.util-linux}/sbin/hwclock --systohc ${if config.time.hardwareClockInLocalTime then "--localtime" else "--utc"}";
       };
     };
 
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index f7c2940049e56..5b39f34200cdc 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -2,6 +2,13 @@
 
 targetRoot=/mnt-root
 console=tty1
+verbose="@verbose@"
+
+info() {
+    if [[ -n "$verbose" ]]; then
+        echo "$@"
+    fi
+}
 
 extraUtils="@extraUtils@"
 export LD_LIBRARY_PATH=@extraUtils@/lib
@@ -55,7 +62,7 @@ EOF
         echo "Rebooting..."
         reboot -f
     else
-        echo "Continuing..."
+        info "Continuing..."
     fi
 }
 
@@ -63,9 +70,9 @@ trap 'fail' 0
 
 
 # Print a greeting.
-echo
-echo "<<< NixOS Stage 1 >>>"
-echo
+info
+info "<<< NixOS Stage 1 >>>"
+info
 
 # Make several required directories.
 mkdir -p /etc/udev
@@ -120,7 +127,7 @@ eval "exec $logOutFd>&1 $logErrFd>&2"
 if test -w /dev/kmsg; then
     tee -i < /tmp/stage-1-init.log.fifo /proc/self/fd/"$logOutFd" | while read -r line; do
         if test -n "$line"; then
-            echo "<7>stage-1-init: $line" > /dev/kmsg
+            echo "<7>stage-1-init: [$(date)] $line" > /dev/kmsg
         fi
     done &
 else
@@ -210,15 +217,18 @@ ln -s @modulesClosure@/lib/modules /lib/modules
 ln -s @modulesClosure@/lib/firmware /lib/firmware
 echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
 for i in @kernelModules@; do
-    echo "loading module $(basename $i)..."
+    info "loading module $(basename $i)..."
     modprobe $i
 done
 
 
 # Create device nodes in /dev.
 @preDeviceCommands@
-echo "running udev..."
+info "running udev..."
 ln -sfn /proc/self/fd /dev/fd
+ln -sfn /proc/self/fd/0 /dev/stdin
+ln -sfn /proc/self/fd/1 /dev/stdout
+ln -sfn /proc/self/fd/2 /dev/stderr
 mkdir -p /etc/systemd
 ln -sfn @linkUnits@ /etc/systemd/network
 mkdir -p /etc/udev
@@ -232,8 +242,7 @@ udevadm settle
 # XXX: Use case usb->lvm will still fail, usb->luks->lvm is covered
 @preLVMCommands@
 
-
-echo "starting device mapper and LVM..."
+info "starting device mapper and LVM..."
 lvm vgchange -ay
 
 if test -n "$debug1devices"; then fail; fi
@@ -356,6 +365,7 @@ mountFS() {
     case $options in
         *x-nixos.autoresize*)
             if [ "$fsType" = ext2 -o "$fsType" = ext3 -o "$fsType" = ext4 ]; then
+                modprobe "$fsType"
                 echo "resizing $device..."
                 e2fsck -fp "$device"
                 resize2fs "$device"
@@ -375,7 +385,7 @@ mountFS() {
         done
     fi
 
-    echo "mounting $device on $mountPoint..."
+    info "mounting $device on $mountPoint..."
 
     mkdir -p "/mnt-root$mountPoint"
 
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 6823e12847c2f..4074f2e02352d 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -22,7 +22,7 @@ let
     rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
     kernel = modulesTree;
     firmware = firmware;
-    allowMissing = true;
+    allowMissing = false;
   };
 
 
@@ -107,8 +107,8 @@ let
         copy_bin_and_libs $BIN
       done
 
-      # Copy some utillinux stuff.
-      copy_bin_and_libs ${pkgs.utillinux}/sbin/blkid
+      # Copy some util-linux stuff.
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid
 
       # Copy dmsetup and lvm.
       copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup
@@ -205,13 +205,22 @@ let
     ''; # */
 
 
+  # Networkd link files are used early by udev to set up interfaces early.
+  # This must be done in stage 1 to avoid race conditions between udev and
+  # network daemons.
   linkUnits = pkgs.runCommand "link-units" {
       allowedReferences = [ extraUtils ];
       preferLocalBuild = true;
-    } ''
+    } (''
       mkdir -p $out
       cp -v ${udev}/lib/systemd/network/*.link $out/
-    '';
+      '' + (
+      let
+        links = filterAttrs (n: v: hasSuffix ".link" n) config.systemd.network.units;
+        files = mapAttrsToList (n: v: "${v.unit}/${n}") links;
+      in
+        concatMapStringsSep "\n" (file: "cp -v ${file} $out/") files
+      ));
 
   udevRules = pkgs.runCommand "udev-rules" {
       allowedReferences = [ extraUtils ];
@@ -235,7 +244,7 @@ let
             --replace scsi_id ${extraUtils}/bin/scsi_id \
             --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.util-linux}/bin/blkid ${extraUtils}/bin/blkid \
             --replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \
             --replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \
             --replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \
@@ -280,7 +289,7 @@ let
 
     inherit (config.system.build) earlyMountScript;
 
-    inherit (config.boot.initrd) checkJournalingFS
+    inherit (config.boot.initrd) checkJournalingFS verbose
       preLVMCommands preDeviceCommands postDeviceCommands postMountCommands preFailCommands kernelModules;
 
     resumeDevices = map (sd: if sd ? device then sd.device else "/dev/disk/by-label/${sd.label}")
@@ -308,7 +317,7 @@ let
   # the initial RAM disk.
   initialRamdisk = pkgs.makeInitrd {
     name = "initrd-${kernel-name}";
-    inherit (config.boot.initrd) compressor prepend;
+    inherit (config.boot.initrd) compressor compressorArgs prepend;
 
     contents =
       [ { object = bootStage1;
@@ -334,7 +343,9 @@ let
 
   # Script to add secret files to the initrd at bootloader update time
   initialRamdiskSecretAppender =
-    pkgs.writeScriptBin "append-initrd-secrets"
+    let
+      compressorExe = initialRamdisk.compressorExecutableFunction pkgs;
+    in pkgs.writeScriptBin "append-initrd-secrets"
       ''
         #!${pkgs.bash}/bin/bash -e
         function usage {
@@ -376,7 +387,7 @@ let
          }
 
         (cd "$tmp" && find . -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null) | \
-          ${config.boot.initrd.compressor} >> "$1"
+          ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
       '';
 
 in
@@ -511,13 +522,33 @@ in
     };
 
     boot.initrd.compressor = mkOption {
-      internal = true;
-      default = "gzip -9n";
-      type = types.str;
-      description = "The compressor to use on the initrd image.";
+      default = (
+        if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9"
+        then "zstd"
+        else "gzip"
+      );
+      defaultText = "zstd if the kernel supports it (5.9+), gzip if not.";
+      type = types.unspecified; # We don't have a function type...
+      description = ''
+        The compressor to use on the initrd image. May be any of:
+
+        <itemizedlist>
+         <listitem><para>The name of one of the predefined compressors, see <filename>pkgs/build-support/kernel/initrd-compressor-meta.nix</filename> for the definitions.</para></listitem>
+         <listitem><para>A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. <literal>pkgs: "''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
+         <listitem><para>(not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. <literal>"''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
+        </itemizedlist>
+
+        The given program should read data from stdin and write it to stdout compressed.
+      '';
       example = "xz";
     };
 
+    boot.initrd.compressorArgs = mkOption {
+      default = null;
+      type = types.nullOr (types.listOf types.str);
+      description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults.";
+    };
+
     boot.initrd.secrets = mkOption
       { default = {};
         type = types.attrsOf (types.nullOr types.path);
@@ -543,6 +574,23 @@ in
       description = "Names of supported filesystem types in the initial ramdisk.";
     };
 
+    boot.initrd.verbose = mkOption {
+      default = true;
+      type = types.bool;
+      description =
+        ''
+          Verbosity of the initrd. Please note that disabling verbosity removes
+          only the mandatory messages generated by the NixOS scripts. For a
+          completely silent boot, you might also want to set the two following
+          configuration options:
+
+          <itemizedlist>
+            <listitem><para><literal>boot.consoleLogLevel = 0;</literal></para></listitem>
+            <listitem><para><literal>boot.kernelParams = [ "quiet" "udev.log_priority=3" ];</literal></para></listitem>
+          </itemizedlist>
+        '';
+    };
+
     boot.loader.supportsInitrdSecrets = mkOption
       { internal = true;
         default = false;
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index dd6d83ee00947..94bc34fea0db3 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -17,7 +17,7 @@ let
     inherit (config.system.build) earlyMountScript;
     path = lib.makeBinPath ([
       pkgs.coreutils
-      pkgs.utillinux
+      pkgs.util-linux
     ] ++ lib.optional useHostResolvConf pkgs.openresolv);
     fsPackagesPath = lib.makeBinPath config.system.fsPackages;
     postBootCommands = pkgs.writeText "local-cmds"
diff --git a/nixos/modules/system/boot/systemd-lib.nix b/nixos/modules/system/boot/systemd-lib.nix
index fa109394fedbe..2dbf15031a088 100644
--- a/nixos/modules/system/boot/systemd-lib.nix
+++ b/nixos/modules/system/boot/systemd-lib.nix
@@ -92,10 +92,12 @@ in rec {
 
   checkUnitConfig = group: checks: attrs: let
     # We're applied at the top-level type (attrsOf unitOption), so the actual
-    # unit options might contain attributes from mkOverride that we need to
+    # unit options might contain attributes from mkOverride and mkIf that we need to
     # convert into single values before checking them.
     defs = mapAttrs (const (v:
-      if v._type or "" == "override" then v.content else v
+      if v._type or "" == "override" then v.content
+      else if v._type or "" == "if" then v.content
+      else v
     )) attrs;
     errors = concatMap (c: c group defs) checks;
   in if errors == [] then true
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index 5addc6f9ca44d..4154389b2ce5f 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -210,12 +210,21 @@ in rec {
       '';
     };
 
+    startLimitBurst = mkOption {
+       type = types.int;
+       description = ''
+         Configure unit start rate limiting. Units which are started
+         more than startLimitBurst times within an interval time
+         interval are not permitted to start any more.
+       '';
+    };
+
     startLimitIntervalSec = mkOption {
        type = types.int;
        description = ''
          Configure unit start rate limiting. Units which are started
-         more than burst times within an interval time interval are
-         not permitted to start any more.
+         more than startLimitBurst times within an interval time
+         interval are not permitted to start any more.
        '';
     };
 
@@ -245,8 +254,7 @@ in rec {
     serviceConfig = mkOption {
       default = {};
       example =
-        { StartLimitInterval = 10;
-          RestartSec = 5;
+        { RestartSec = 5;
         };
       type = types.addCheck (types.attrsOf unitOption) checkService;
       description = ''
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 74d6957678f56..6b672c7b2eb48 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -243,6 +243,8 @@ let
           OnFailure = toString config.onFailure; }
         // optionalAttrs (options.startLimitIntervalSec.isDefined) {
           StartLimitIntervalSec = toString config.startLimitIntervalSec;
+        } // optionalAttrs (options.startLimitBurst.isDefined) {
+          StartLimitBurst = toString config.startLimitBurst;
         };
     };
   };
@@ -261,7 +263,7 @@ let
         }
         (mkIf (config.preStart != "")
           { serviceConfig.ExecStartPre =
-              makeJobScript "${name}-pre-start" config.preStart;
+              [ (makeJobScript "${name}-pre-start" config.preStart) ];
           })
         (mkIf (config.script != "")
           { serviceConfig.ExecStart =
@@ -269,7 +271,7 @@ let
           })
         (mkIf (config.postStart != "")
           { serviceConfig.ExecStartPost =
-              makeJobScript "${name}-post-start" config.postStart;
+              [ (makeJobScript "${name}-post-start" config.postStart) ];
           })
         (mkIf (config.reload != "")
           { serviceConfig.ExecReload =
@@ -548,6 +550,14 @@ in
       '';
     };
 
+    systemd.enableUnifiedCgroupHierarchy = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable the unified cgroup hierarchy (cgroupsv2).
+      '';
+    };
+
     systemd.coredump.enable = mkOption {
       default = true;
       type = types.bool;
@@ -884,14 +894,25 @@ in
 
   config = {
 
-    warnings = concatLists (mapAttrsToList (name: service:
-      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);
+    warnings = concatLists (
+      mapAttrsToList
+        (name: service:
+          let
+            type = service.serviceConfig.Type or "";
+            restart = service.serviceConfig.Restart or "no";
+            hasDeprecated = builtins.hasAttr "StartLimitInterval" service.serviceConfig;
+          in
+            concatLists [
+              (optional (type == "oneshot" && (restart == "always" || restart == "on-success"))
+                "Service '${name}.service' with 'Type=oneshot' cannot have 'Restart=always' or 'Restart=on-success'"
+              )
+              (optional hasDeprecated
+                "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786."
+              )
+            ]
+        )
+        cfg.services
+    );
 
     system.build.units = cfg.units;
 
@@ -1165,6 +1186,7 @@ in
     boot.kernel.sysctl = mkIf (!cfg.coredump.enable) {
       "kernel.core_pattern" = "core";
     };
+    boot.kernelParams = optional (!cfg.enableUnifiedCgroupHierarchy) "systemd.unified_cgroup_hierarchy=0";
   };
 
   # FIXME: Remove these eventually.
diff --git a/nixos/modules/system/boot/timesyncd.nix b/nixos/modules/system/boot/timesyncd.nix
index 35fb5578b0705..692315dbe99c4 100644
--- a/nixos/modules/system/boot/timesyncd.nix
+++ b/nixos/modules/system/boot/timesyncd.nix
@@ -16,6 +16,7 @@ with lib;
       };
       servers = mkOption {
         default = config.networking.timeServers;
+        type = types.listOf types.str;
         description = ''
           The set of NTP servers from which to synchronise.
         '';
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 26eb172210e73..5bb299adb15f0 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -30,7 +30,14 @@ with lib;
 
   config = {
 
-    systemd.additionalUpstreamSystemUnits = optional config.boot.tmpOnTmpfs "tmp.mount";
+    systemd.mounts = mkIf config.boot.tmpOnTmpfs [
+      {
+        what = "tmpfs";
+        where = "/tmp";
+        type = "tmpfs";
+        mountConfig.Options = [ "mode=1777" "strictatime" "rw" "nosuid" "nodev" "size=50%" ];
+      }
+    ];
 
     systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
 
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
index 69385e5f2fe01..b19b688a1fb8a 100644
--- a/nixos/modules/tasks/auto-upgrade.nix
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -109,9 +109,8 @@ in {
       '';
     }];
 
-    system.autoUpgrade.flags = [ "--no-build-output" ]
-      ++ (if cfg.flake == null then
-        (if cfg.channel == null then
+    system.autoUpgrade.flags = (if cfg.flake == null then
+        [ "--no-build-output" ] ++ (if cfg.channel == null then
           [ "--upgrade" ]
         else [
           "-I"
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 3ea67dac7146d..a9b5b134d889d 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -7,8 +7,9 @@ let
 
   addCheckDesc = desc: elemType: check: types.addCheck elemType check
     // { description = "${elemType.description} (with check: ${desc})"; };
-  nonEmptyStr = addCheckDesc "non-empty" types.str
-    (x: x != "" && ! (all (c: c == " " || c == "\t") (stringToCharacters x)));
+
+  isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
+  nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
 
   fileSystems' = toposort fsBefore (attrValues config.fileSystems);
 
@@ -28,10 +29,10 @@ let
   coreFileSystemOpts = { name, config, ... }: {
 
     options = {
-
       mountPoint = mkOption {
         example = "/mnt/usb";
-        type = nonEmptyStr;
+        type = addCheckDesc "non-empty without trailing slash" types.str
+          (s: isNonEmpty s && (builtins.match ".+/" s) == null);
         description = "Location of the mounted the file system.";
       };
 
@@ -286,7 +287,7 @@ in
             before = [ mountPoint' "systemd-fsck@${device'}.service" ];
             requires = [ device'' ];
             after = [ device'' ];
-            path = [ pkgs.utillinux ] ++ config.system.fsPackages;
+            path = [ pkgs.util-linux ] ++ config.system.fsPackages;
             script =
               ''
                 if ! [ -e "${fs.device}" ]; then exit 1; fi
diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix
index 5fda24adb9782..ac41ba5f93a4b 100644
--- a/nixos/modules/tasks/filesystems/bcachefs.nix
+++ b/nixos/modules/tasks/filesystems/bcachefs.nix
@@ -49,8 +49,8 @@ in
     }
 
     (mkIf ((elem "bcachefs" config.boot.initrd.supportedFilesystems) || (bootFs != {})) {
-      # the cryptographic modules are required only for decryption attempts
-      boot.initrd.availableKernelModules = [ "bcachefs" "chacha20" "poly1305" ];
+      # chacha20 and poly1305 are required only for decryption attempts
+      boot.initrd.availableKernelModules = [ "bcachefs" "sha256" "chacha20" "poly1305" ];
 
       boot.initrd.extraUtilsCommands = ''
         copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix
index ddcc0ed8f5a43..fd35c35d32adc 100644
--- a/nixos/modules/tasks/filesystems/nfs.nix
+++ b/nixos/modules/tasks/filesystems/nfs.nix
@@ -10,20 +10,9 @@ let
 
   rpcMountpoint = "${nfsStateDir}/rpc_pipefs";
 
-  idmapdConfFile = pkgs.writeText "idmapd.conf" ''
-    [General]
-    Pipefs-Directory = ${rpcMountpoint}
-    ${optionalString (config.networking.domain != null)
-      "Domain = ${config.networking.domain}"}
-
-    [Mapping]
-    Nobody-User = nobody
-    Nobody-Group = nogroup
-
-    [Translation]
-    Method = nsswitch
-  '';
+  format = pkgs.formats.ini {};
 
+  idmapdConfFile = format.generate "idmapd.conf" cfg.idmapd.settings;
   nfsConfFile = pkgs.writeText "nfs.conf" cfg.extraConfig;
   requestKeyConfFile = pkgs.writeText "request-key.conf" ''
     create id_resolver * * ${pkgs.nfs-utils}/bin/nfsidmap -t 600 %k %d
@@ -38,6 +27,25 @@ in
 
   options = {
     services.nfs = {
+      idmapd.settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          libnfsidmap configuration. Refer to
+          <link xlink:href="https://linux.die.net/man/5/idmapd.conf"/>
+          for details.
+        '';
+        example = literalExample ''
+          {
+            Translation = {
+              GSS-Methods = "static,nsswitch";
+            };
+            Static = {
+              "root/hostname.domain.com@REALM.COM" = "root";
+            };
+          }
+        '';
+      };
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -54,6 +62,20 @@ in
 
     services.rpcbind.enable = true;
 
+    services.nfs.idmapd.settings = {
+      General = mkMerge [
+        { Pipefs-Directory = rpcMountpoint; }
+        (mkIf (config.networking.domain != null) { Domain = config.networking.domain; })
+      ];
+      Mapping = {
+        Nobody-User = "nobody";
+        Nobody-Group = "nogroup";
+      };
+      Translation = {
+        Method = "nsswitch";
+      };
+    };
+
     system.fsPackages = [ pkgs.nfs-utils ];
 
     boot.initrd.kernelModules = mkIf inInitrd [ "nfs" ];
diff --git a/nixos/modules/tasks/filesystems/unionfs-fuse.nix b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
index 1dcc4c87e3cec..f54f3559c3411 100644
--- a/nixos/modules/tasks/filesystems/unionfs-fuse.nix
+++ b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
@@ -18,9 +18,9 @@
 
       boot.initrd.postDeviceCommands = ''
           # Hacky!!! fuse hard-codes the path to mount
-          mkdir -p /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.utillinux.name}-bin/bin
-          ln -s $(which mount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.utillinux.name}-bin/bin
-          ln -s $(which umount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.utillinux.name}-bin/bin
+          mkdir -p /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
+          ln -s $(which mount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
+          ln -s $(which umount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
         '';
     })
 
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 9ca7c6fb3431b..59676e996785c 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -17,20 +17,8 @@ let
   inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems;
   inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems;
 
-  enableZfs = inInitrd || inSystem;
-
-  kernel = config.boot.kernelPackages;
-
-  packages = if config.boot.zfs.enableUnstable then {
-    zfs = kernel.zfsUnstable;
-    zfsUser = pkgs.zfsUnstable;
-  } else {
-    zfs = kernel.zfs;
-    zfsUser = pkgs.zfs;
-  };
-
   autosnapPkg = pkgs.zfstools.override {
-    zfs = packages.zfsUser;
+    zfs = cfgZfs.package;
   };
 
   zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot";
@@ -111,6 +99,20 @@ in
 
   options = {
     boot.zfs = {
+      package = mkOption {
+        readOnly = true;
+        type = types.package;
+        default = if config.boot.zfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs;
+        description = "Configured ZFS userland tools package.";
+      };
+
+      enabled = mkOption {
+        readOnly = true;
+        type = types.bool;
+        default = inInitrd || inSystem;
+        description = "True if ZFS filesystem support is enabled";
+      };
+
       enableUnstable = mkOption {
         type = types.bool;
         default = false;
@@ -175,14 +177,10 @@ in
 
       forceImportAll = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Forcibly import all ZFS pool(s).
 
-          This is enabled by default for backwards compatibility purposes, but it is highly
-          recommended to disable this option, as it bypasses some of the safeguards ZFS uses
-          to protect your ZFS pools.
-
           If you set this option to <literal>false</literal> and NixOS subsequently fails to
           import your non-root ZFS pool(s), you should manually import each pool with
           "zpool import -f &lt;pool-name&gt;", and then reboot. You should only need to do
@@ -328,39 +326,53 @@ in
       };
     };
 
-    services.zfs.zed.settings = mkOption {
-      type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
-      example = literalExample ''
-        {
-          ZED_DEBUG_LOG = "/tmp/zed.debug.log";
+    services.zfs.zed = {
+      enableMail = mkEnableOption "ZED's ability to send emails" // {
+        default = cfgZfs.package.enableMail;
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        example = literalExample ''
+          {
+            ZED_DEBUG_LOG = "/tmp/zed.debug.log";
 
-          ZED_EMAIL_ADDR = [ "root" ];
-          ZED_EMAIL_PROG = "mail";
-          ZED_EMAIL_OPTS = "-s '@SUBJECT@' @ADDRESS@";
+            ZED_EMAIL_ADDR = [ "root" ];
+            ZED_EMAIL_PROG = "mail";
+            ZED_EMAIL_OPTS = "-s '@SUBJECT@' @ADDRESS@";
 
-          ZED_NOTIFY_INTERVAL_SECS = 3600;
-          ZED_NOTIFY_VERBOSE = false;
+            ZED_NOTIFY_INTERVAL_SECS = 3600;
+            ZED_NOTIFY_VERBOSE = false;
 
-          ZED_USE_ENCLOSURE_LEDS = true;
-          ZED_SCRUB_AFTER_RESILVER = false;
-        }
-      '';
-      description = ''
-        ZFS Event Daemon /etc/zfs/zed.d/zed.rc content
-
-        See
-        <citerefentry><refentrytitle>zed</refentrytitle><manvolnum>8</manvolnum></citerefentry>
-        for details on ZED and the scripts in /etc/zfs/zed.d to find the possible variables
-      '';
+            ZED_USE_ENCLOSURE_LEDS = true;
+            ZED_SCRUB_AFTER_RESILVER = false;
+          }
+        '';
+        description = ''
+          ZFS Event Daemon /etc/zfs/zed.d/zed.rc content
+
+          See
+          <citerefentry><refentrytitle>zed</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+          for details on ZED and the scripts in /etc/zfs/zed.d to find the possible variables
+        '';
+      };
     };
   };
 
   ###### implementation
 
   config = mkMerge [
-    (mkIf enableZfs {
+    (mkIf cfgZfs.enabled {
       assertions = [
         {
+          assertion = cfgZED.enableMail -> cfgZfs.package.enableMail;
+          message = ''
+            To allow ZED to send emails, ZFS needs to be configured to enable
+            this. To do so, one must override the `zfs` package and set
+            `enableMail` to true.
+          '';
+        }
+        {
           assertion = config.networking.hostId != null;
           message = "ZFS requires networking.hostId to be set";
         }
@@ -370,20 +382,24 @@ in
         }
       ];
 
-      virtualisation.lxd.zfsSupport = true;
-
       boot = {
         kernelModules = [ "zfs" ];
-        extraModulePackages = with packages; [ zfs ];
+
+        extraModulePackages = [
+          (if config.boot.zfs.enableUnstable then
+            config.boot.kernelPackages.zfsUnstable
+           else
+            config.boot.kernelPackages.zfs)
+        ];
       };
 
       boot.initrd = mkIf inInitrd {
         kernelModules = [ "zfs" ] ++ optional (!cfgZfs.enableUnstable) "spl";
         extraUtilsCommands =
           ''
-            copy_bin_and_libs ${packages.zfsUser}/sbin/zfs
-            copy_bin_and_libs ${packages.zfsUser}/sbin/zdb
-            copy_bin_and_libs ${packages.zfsUser}/sbin/zpool
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zfs
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zdb
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zpool
           '';
         extraUtilsCommandsTest = mkIf inInitrd
           ''
@@ -435,16 +451,16 @@ in
       };
 
       services.zfs.zed.settings = {
-        ZED_EMAIL_PROG = mkDefault "${pkgs.mailutils}/bin/mail";
+        ZED_EMAIL_PROG = mkIf cfgZED.enableMail (mkDefault "${pkgs.mailutils}/bin/mail");
         PATH = lib.makeBinPath [
-          packages.zfsUser
+          cfgZfs.package
           pkgs.coreutils
           pkgs.curl
           pkgs.gawk
           pkgs.gnugrep
           pkgs.gnused
           pkgs.nettools
-          pkgs.utillinux
+          pkgs.util-linux
         ];
       };
 
@@ -465,18 +481,18 @@ in
             "vdev_clear-led.sh"
           ]
         )
-        (file: { source = "${packages.zfsUser}/etc/${file}"; })
+        (file: { source = "${cfgZfs.package}/etc/${file}"; })
       // {
         "zfs/zed.d/zed.rc".text = zedConf;
-        "zfs/zpool.d".source = "${packages.zfsUser}/etc/zfs/zpool.d/";
+        "zfs/zpool.d".source = "${cfgZfs.package}/etc/zfs/zpool.d/";
       };
 
-      system.fsPackages = [ packages.zfsUser ]; # XXX: needed? zfs doesn't have (need) a fsck
-      environment.systemPackages = [ packages.zfsUser ]
+      system.fsPackages = [ cfgZfs.package ]; # XXX: needed? zfs doesn't have (need) a fsck
+      environment.systemPackages = [ cfgZfs.package ]
         ++ optional cfgSnapshots.enable autosnapPkg; # so the user can run the command to see flags
 
-      services.udev.packages = [ packages.zfsUser ]; # to hook zvol naming, etc.
-      systemd.packages = [ packages.zfsUser ];
+      services.udev.packages = [ cfgZfs.package ]; # to hook zvol naming, etc.
+      systemd.packages = [ cfgZfs.package ];
 
       systemd.services = let
         getPoolFilesystems = pool:
@@ -507,10 +523,11 @@ in
               Type = "oneshot";
               RemainAfterExit = true;
             };
+            environment.ZFS_FORCE = optionalString cfgZfs.forceImportAll "-f";
             script = (importLib {
               # See comments at importLib definition.
-              zpoolCmd="${packages.zfsUser}/sbin/zpool";
-              awkCmd="${pkgs.gawk}/bin/awk";
+              zpoolCmd = "${cfgZfs.package}/sbin/zpool";
+              awkCmd = "${pkgs.gawk}/bin/awk";
               inherit cfgZfs;
             }) + ''
               poolImported "${pool}" && exit
@@ -525,7 +542,7 @@ in
                 ${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
+                  ${cfgZfs.package}/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
@@ -535,10 +552,10 @@ in
                       none )
                         ;;
                       prompt )
-                        ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${packages.zfsUser}/sbin/zfs load-key "$ds"
+                        ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds"
                         ;;
                       * )
-                        ${packages.zfsUser}/sbin/zfs load-key "$ds"
+                        ${cfgZfs.package}/sbin/zfs load-key "$ds"
                         ;;
                     esac) < /dev/null # To protect while read ds kl in case anything reads stdin
                   done
@@ -564,7 +581,7 @@ in
               RemainAfterExit = true;
             };
             script = ''
-              ${packages.zfsUser}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}"
+              ${cfgZfs.package}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}"
             '';
           };
         createZfsService = serv:
@@ -590,7 +607,7 @@ in
       systemd.targets.zfs.wantedBy = [ "multi-user.target" ];
     })
 
-    (mkIf (enableZfs && cfgSnapshots.enable) {
+    (mkIf (cfgZfs.enabled && cfgSnapshots.enable) {
       systemd.services = let
                            descr = name: if name == "frequent" then "15 mins"
                                     else if name == "hourly" then "hour"
@@ -628,7 +645,7 @@ in
                             }) snapshotNames);
     })
 
-    (mkIf (enableZfs && cfgScrub.enable) {
+    (mkIf (cfgZfs.enabled && cfgScrub.enable) {
       systemd.services.zfs-scrub = {
         description = "ZFS pools scrubbing";
         after = [ "zfs-import.target" ];
@@ -636,11 +653,11 @@ in
           Type = "oneshot";
         };
         script = ''
-          ${packages.zfsUser}/bin/zpool scrub ${
+          ${cfgZfs.package}/bin/zpool scrub ${
             if cfgScrub.pools != [] then
               (concatStringsSep " " cfgScrub.pools)
             else
-              "$(${packages.zfsUser}/bin/zpool list -H -o name)"
+              "$(${cfgZfs.package}/bin/zpool list -H -o name)"
             }
         '';
       };
@@ -655,18 +672,20 @@ in
       };
     })
 
-    (mkIf (enableZfs && cfgTrim.enable) {
+    (mkIf (cfgZfs.enabled && cfgTrim.enable) {
       systemd.services.zpool-trim = {
         description = "ZFS pools trim";
         after = [ "zfs-import.target" ];
-        path = [ packages.zfsUser ];
+        path = [ cfgZfs.package ];
         startAt = cfgTrim.interval;
         # By default we ignore errors returned by the trim command, in case:
         # - HDDs are mixed with SSDs
         # - There is a SSDs in a pool that is currently trimmed.
         # - There are only HDDs and we would set the system in a degraded state
-        serviceConfig.ExecStart = ''${pkgs.runtimeShell} -c 'for pool in $(zpool list -H -o name); do zpool trim $pool;  done || true' '';
+        serviceConfig.ExecStart = "${pkgs.runtimeShell} -c 'for pool in $(zpool list -H -o name); do zpool trim $pool;  done || true' ";
       };
+
+      systemd.timers.zpool-trim.timerConfig.Persistent = "yes";
     })
   ];
 }
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
index 2c3cc4c5467dc..98a0e2ddef900 100644
--- a/nixos/modules/tasks/lvm.nix
+++ b/nixos/modules/tasks/lvm.nix
@@ -21,6 +21,10 @@ in {
   };
 
   config = mkMerge [
+    ({
+      # minimal configuration file to make lvmconfig/lvm2-activation-generator happy
+      environment.etc."lvm/lvm.conf".text = "config {}";
+    })
     (mkIf (!config.boot.isContainer) {
       systemd.tmpfiles.packages = [ cfg.package.out ];
       environment.systemPackages = [ cfg.package ];
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index 23e1e611a71e9..088bffd7c5084 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -93,7 +93,17 @@ in
             (if i.useDHCP != null then i.useDHCP else false));
           address = forEach (interfaceIps i)
             (ip: "${ip.address}/${toString ip.prefixLength}");
-          networkConfig.IPv6PrivacyExtensions = "kernel";
+          # IPv6PrivacyExtensions=kernel seems to be broken with networkd.
+          # Instead of using IPv6PrivacyExtensions=kernel, configure it according to the value of
+          # `tempAddress`:
+          networkConfig.IPv6PrivacyExtensions = {
+            # generate temporary addresses and use them by default
+            "default" = true;
+            # generate temporary addresses but keep using the standard EUI-64 ones by default
+            "enabled" = "prefer-public";
+            # completely disable temporary addresses
+            "disabled" = false;
+          }.${i.tempAddress};
           linkConfig = optionalAttrs (i.macAddress != null) {
             MACAddress = i.macAddress;
           } // optionalAttrs (i.mtu != null) {
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index e5bd577536833..f730ec82bdf5c 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -398,6 +398,24 @@ in
       '';
     };
 
+    networking.fqdn = mkOption {
+      readOnly = true;
+      type = types.str;
+      default = if (cfg.hostName != "" && cfg.domain != null)
+        then "${cfg.hostName}.${cfg.domain}"
+        else throw ''
+          The FQDN is required but cannot be determined. Please make sure that
+          both networking.hostName and networking.domain are set properly.
+        '';
+      defaultText = literalExample ''''${networking.hostName}.''${networking.domain}'';
+      description = ''
+        The fully qualified domain name (FQDN) of this host. It is the result
+        of combining networking.hostName and networking.domain. Using this
+        option will result in an evaluation error if the hostname is empty or
+        no domain is specified.
+      '';
+    };
+
     networking.hostId = mkOption {
       default = null;
       example = "4e98920d";
@@ -1062,7 +1080,6 @@ in
       ];
 
     boot.kernelModules = [ ]
-      ++ optional cfg.enableIPv6 "ipv6"
       ++ optional hasVirtuals "tun"
       ++ optional hasSits "sit"
       ++ optional hasBonds "bonding";
@@ -1247,7 +1264,7 @@ in
             '';
 
             # Udev attributes for systemd to name the device and to create a .device target.
-            systemdAttrs = n: ''NAME:="${n}", ENV{INTERFACE}:="${n}", ENV{SYSTEMD_ALIAS}:="/sys/subsystem/net/devices/${n}", TAG+="systemd"'';
+            systemdAttrs = n: ''NAME:="${n}", ENV{INTERFACE}="${n}", ENV{SYSTEMD_ALIAS}="/sys/subsystem/net/devices/${n}", TAG+="systemd"'';
           in
           flip (concatMapStringsSep "\n") (attrNames wlanDeviceInterfaces) (device:
             let
diff --git a/nixos/modules/testing/service-runner.nix b/nixos/modules/testing/service-runner.nix
index 99a9f979068da..76e9d4a68c416 100644
--- a/nixos/modules/testing/service-runner.nix
+++ b/nixos/modules/testing/service-runner.nix
@@ -52,7 +52,7 @@ let
 
       # Run the ExecStartPre program.  FIXME: this could be a list.
       my $preStart = <<END_CMD;
-      ${service.serviceConfig.ExecStartPre or ""}
+      ${concatStringsSep "\n" (service.serviceConfig.ExecStartPre or [])}
       END_CMD
       if (defined $preStart && $preStart ne "\n") {
           print STDERR "running ExecStartPre: $preStart\n";
@@ -79,7 +79,7 @@ let
 
       # Run the ExecStartPost program.
       my $postStart = <<END_CMD;
-      ${service.serviceConfig.ExecStartPost or ""}
+      ${concatStringsSep "\n" (service.serviceConfig.ExecStartPost or [])}
       END_CMD
       if (defined $postStart && $postStart ne "\n") {
           print STDERR "running ExecStartPost: $postStart\n";
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index c0ec76e8a3a35..be5fa88b8ade1 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -45,13 +45,22 @@ with import ../../lib/qemu-flags.nix { inherit pkgs; };
     systemd.services."serial-getty@${qemuSerialDevice}".enable = false;
     systemd.services."serial-getty@hvc0".enable = false;
 
-    # Only use a serial console, no TTY.
-    # 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 ]; };
+    # Only set these settings when the options exist. Some tests (e.g. those
+    # that do not specify any nodes, or an empty attr set as nodes) will not
+    # have the QEMU module loaded and thuse these options can't and should not
+    # be set.
+    virtualisation = lib.optionalAttrs (options ? virtualisation.qemu) {
+      qemu = {
+        # Only use a serial console, no TTY.
+        # 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
+        consoles = [ qemuSerialDevice ];
+        package  = lib.mkDefault pkgs.qemu_test;
+      };
+    };
 
     boot.initrd.preDeviceCommands =
       ''
@@ -116,6 +125,10 @@ with import ../../lib/qemu-flags.nix { inherit pkgs; };
     users.users.root.initialHashedPassword = mkOverride 150 "";
 
     services.xserver.displayManager.job.logToJournal = true;
+
+    # Make sure we use the Guest Agent from the QEMU package for testing
+    # to reduce the closure size required for the tests.
+    services.qemuGuest.package = pkgs.qemu_test.ga;
   };
 
 }
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index 20d48add71293..26297a7d0f1f7 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -11,6 +11,7 @@ with lib;
 let
   cfg = config.ec2;
   metadataFetcher = import ./ec2-metadata-fetcher.nix {
+    inherit (pkgs) curl;
     targetRoot = "$targetRoot/";
     wgetExtraOptions = "-q";
   };
@@ -48,7 +49,7 @@ in
     ];
     boot.initrd.kernelModules = [ "xen-blkfront" "xen-netfront" ];
     boot.initrd.availableKernelModules = [ "ixgbevf" "ena" "nvme" ];
-    boot.kernelParams = mkIf cfg.hvm [ "console=ttyS0" ];
+    boot.kernelParams = mkIf cfg.hvm [ "console=ttyS0" "random.trust_cpu=on" ];
 
     # Prevent the nouveau kernel module from being loaded, as it
     # interferes with the nvidia/nvidia-uvm modules needed for CUDA.
@@ -123,7 +124,7 @@ in
     boot.initrd.extraUtilsCommands =
       ''
         # We need swapon in the initrd.
-        copy_bin_and_libs ${pkgs.utillinux}/sbin/swapon
+        copy_bin_and_libs ${pkgs.util-linux}/sbin/swapon
       '';
 
     # Don't put old configurations in the GRUB menu.  The user has no
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
index 8c12e0e49bf5b..c5470b7af09b0 100644
--- a/nixos/modules/virtualisation/amazon-init.nix
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -7,7 +7,7 @@ let
     echo "attempting to fetch configuration from EC2 user data..."
 
     export HOME=/root
-    export PATH=${pkgs.lib.makeBinPath [ config.nix.package pkgs.systemd pkgs.gnugrep pkgs.git pkgs.gnutar pkgs.gzip pkgs.gnused config.system.build.nixos-rebuild]}:$PATH
+    export PATH=${pkgs.lib.makeBinPath [ config.nix.package pkgs.systemd pkgs.gnugrep pkgs.git pkgs.gnutar pkgs.gzip pkgs.gnused pkgs.xz config.system.build.nixos-rebuild]}:$PATH
     export NIX_PATH=nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels
 
     userData=/etc/ec2-metadata/user-data
diff --git a/nixos/modules/virtualisation/azure-agent.nix b/nixos/modules/virtualisation/azure-agent.nix
index e85482af8392e..41f3fa0e6642e 100644
--- a/nixos/modules/virtualisation/azure-agent.nix
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -22,7 +22,7 @@ let
                     nettools # for hostname
                     procps # for pidof
                     shadow # for useradd, usermod
-                    utillinux # for (u)mount, fdisk, sfdisk, mkswap
+                    util-linux # for (u)mount, fdisk, sfdisk, mkswap
                     parted
                   ];
     pythonPath = [ pythonPackages.pyasn1 ];
@@ -146,7 +146,7 @@ in
 
     services.logrotate = {
       enable = true;
-      config = ''
+      extraConfig = ''
         /var/log/waagent.log {
             compress
             monthly
diff --git a/nixos/modules/virtualisation/brightbox-image.nix b/nixos/modules/virtualisation/brightbox-image.nix
index d0efbcc808aa1..4498e3a736185 100644
--- a/nixos/modules/virtualisation/brightbox-image.nix
+++ b/nixos/modules/virtualisation/brightbox-image.nix
@@ -27,7 +27,7 @@ in
               popd
             '';
           diskImageBase = "nixos-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.raw";
-          buildInputs = [ pkgs.utillinux pkgs.perl ];
+          buildInputs = [ pkgs.util-linux pkgs.perl ];
           exportReferencesGraph =
             [ "closure" config.system.build.toplevel ];
         }
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index aa416e7990a8b..8d352e36ef99a 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -103,7 +103,10 @@ in
       cgroup_manager = "systemd"
       log_level = "${cfg.logLevel}"
       pinns_path = "${cfg.package}/bin/pinns"
-      hooks_dir = []
+      hooks_dir = [
+      ${lib.optionalString config.virtualisation.containers.ociSeccompBpfHook.enable
+        ''"${config.boot.kernelPackages.oci-seccomp-bpf-hook}",''}
+      ]
 
       ${optionalString (cfg.runtime != null) ''
       default_runtime = "${cfg.runtime}"
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index d87ada35a0ae4..b1415bf021dd9 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -157,8 +157,10 @@ in
 
       systemd.services.docker = {
         wantedBy = optional cfg.enableOnBoot "multi-user.target";
+        requires = [ "docker.socket" ];
         environment = proxy_env;
         serviceConfig = {
+          Type = "notify";
           ExecStart = [
             ""
             ''
@@ -212,13 +214,10 @@ in
           message = "Option enableNvidia requires 32bit support libraries";
         }];
     }
-    (mkIf cfg.enableNvidia {
-      environment.etc."nvidia-container-runtime/config.toml".source = "${pkgs.nvidia-docker}/etc/config.toml";
-    })
   ]);
 
   imports = [
-    (mkRemovedOptionModule ["virtualisation" "docker" "socketActivation"] "This option was removed in favor of starting docker at boot")
+    (mkRemovedOptionModule ["virtualisation" "docker" "socketActivation"] "This option was removed and socket activation is now always active")
   ];
 
 }
diff --git a/nixos/modules/virtualisation/ec2-amis.nix b/nixos/modules/virtualisation/ec2-amis.nix
index 24de8cf1afbf6..892af513b0320 100644
--- a/nixos/modules/virtualisation/ec2-amis.nix
+++ b/nixos/modules/virtualisation/ec2-amis.nix
@@ -329,5 +329,24 @@ let self = {
   "20.03".ap-east-1.hvm-ebs = "ami-0d18fdd309cdefa86";
   "20.03".sa-east-1.hvm-ebs = "ami-09859378158ae971d";
 
-  latest = self."20.03";
+  # 20.09.2016.19db3e5ea27
+  "20.09".eu-west-1.hvm-ebs = "ami-0057cb7d614329fa2";
+  "20.09".eu-west-2.hvm-ebs = "ami-0d46f16e0bb0ec8fd";
+  "20.09".eu-west-3.hvm-ebs = "ami-0e8985c3ea42f87fe";
+  "20.09".eu-central-1.hvm-ebs = "ami-0eed77c38432886d2";
+  "20.09".eu-north-1.hvm-ebs = "ami-0be5bcadd632bea14";
+  "20.09".us-east-1.hvm-ebs = "ami-0a2cce52b42daccc8";
+  "20.09".us-east-2.hvm-ebs = "ami-09378bf487b07a4d8";
+  "20.09".us-west-1.hvm-ebs = "ami-09b4337b2a9e77485";
+  "20.09".us-west-2.hvm-ebs = "ami-081d3bb5fbee0a1ac";
+  "20.09".ca-central-1.hvm-ebs = "ami-020c24c6c607e7ac7";
+  "20.09".ap-southeast-1.hvm-ebs = "ami-08f648d5db009e67d";
+  "20.09".ap-southeast-2.hvm-ebs = "ami-0be390efaccbd40f9";
+  "20.09".ap-northeast-1.hvm-ebs = "ami-0c3311601cbe8f927";
+  "20.09".ap-northeast-2.hvm-ebs = "ami-0020146701f4d56cf";
+  "20.09".ap-south-1.hvm-ebs = "ami-0117e2bd876bb40d1";
+  "20.09".ap-east-1.hvm-ebs = "ami-0c42f97e5b1fda92f";
+  "20.09".sa-east-1.hvm-ebs = "ami-021637976b094959d";
+
+  latest = self."20.09";
 }; in self
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
index b531787c31a29..dca5c2abd4e0c 100644
--- a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
@@ -1,23 +1,77 @@
-{ targetRoot, wgetExtraOptions }:
+{ curl, targetRoot, wgetExtraOptions }:
+# Note: be very cautious about dependencies, each dependency grows
+# the closure of the initrd. Ideally we would not even require curl,
+# but there is no reasonable way to send an HTTP PUT request without
+# it. Note: do not be fooled: the wget referenced in this script
+# is busybox's wget, not the fully featured one with --method support.
+#
+# Make sure that every package you depend on here is already listed as
+# a channel blocker for both the full-sized and small channels.
+# Otherwise, we risk breaking user deploys in released channels.
+#
+# Also note: OpenStack's metadata service for its instances aims to be
+# compatible with the EC2 IMDS. Where possible, try to keep the set of
+# fetched metadata in sync with ./openstack-metadata-fetcher.nix .
 ''
   metaDir=${targetRoot}etc/ec2-metadata
   mkdir -m 0755 -p "$metaDir"
+  rm -f "$metaDir/*"
 
-  echo "getting EC2 instance metadata..."
+  get_imds_token() {
+    # retry-delay of 1 selected to give the system a second to get going,
+    # but not add a lot to the bootup time
+    ${curl}/bin/curl \
+      -v \
+      --retry 3 \
+      --retry-delay 1 \
+      --fail \
+      -X PUT \
+      --connect-timeout 1 \
+      -H "X-aws-ec2-metadata-token-ttl-seconds: 600" \
+      http://169.254.169.254/latest/api/token
+  }
 
-  if ! [ -e "$metaDir/ami-manifest-path" ]; then
-    wget ${wgetExtraOptions} -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
-  fi
+  preflight_imds_token() {
+    # retry-delay of 1 selected to give the system a second to get going,
+    # but not add a lot to the bootup time
+    ${curl}/bin/curl \
+      -v \
+      --retry 3 \
+      --retry-delay 1 \
+      --fail \
+      --connect-timeout 1 \
+      -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" \
+      http://169.254.169.254/1.0/meta-data/instance-id
+  }
 
-  if ! [ -e "$metaDir/user-data" ]; then
-    wget ${wgetExtraOptions} -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
-  fi
+  try=1
+  while [ $try -le 3 ]; do
+    echo "(attempt $try/3) getting an EC2 instance metadata service v2 token..."
+    IMDS_TOKEN=$(get_imds_token) && break
+    try=$((try + 1))
+    sleep 1
+  done
 
-  if ! [ -e "$metaDir/hostname" ]; then
-    wget ${wgetExtraOptions} -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  if [ "x$IMDS_TOKEN" == "x" ]; then
+    echo "failed to fetch an IMDS2v token."
   fi
 
-  if ! [ -e "$metaDir/public-keys-0-openssh-key" ]; then
-    wget ${wgetExtraOptions} -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
-  fi
+  try=1
+  while [ $try -le 10 ]; do
+    echo "(attempt $try/10) validating the EC2 instance metadata service v2 token..."
+    preflight_imds_token && break
+    try=$((try + 1))
+    sleep 1
+  done
+
+  echo "getting EC2 instance metadata..."
+
+  wget_imds() {
+    wget ${wgetExtraOptions} --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@";
+  }
+
+  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
+  wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
+  wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
 ''
diff --git a/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash b/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash
new file mode 100644
index 0000000000000..4a8601961115b
--- /dev/null
+++ b/nixos/modules/virtualisation/fetch-instance-ssh-keys.bash
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+WGET() {
+    wget --retry-connrefused -t 15 --waitretry=10 --header='Metadata-Flavor: Google' "$@"
+}
+
+# When dealing with cryptographic keys, we want to keep things private.
+umask 077
+mkdir -p /root/.ssh
+
+echo "Fetching authorized keys..."
+WGET -O /tmp/auth_keys http://metadata.google.internal/computeMetadata/v1/instance/attributes/sshKeys
+
+# Read keys one by one, split in case Google decided
+# to append metadata (it does sometimes) and add to
+# authorized_keys if not already present.
+touch /root/.ssh/authorized_keys
+while IFS='' read -r line || [[ -n "$line" ]]; do
+    keyLine=$(echo -n "$line" | cut -d ':' -f2)
+    IFS=' ' read -r -a array <<<"$keyLine"
+    if [[ ${#array[@]} -ge 3 ]]; then
+        echo "${array[@]:0:3}" >>/tmp/new_keys
+        echo "Added ${array[*]:2} to authorized_keys"
+    fi
+done </tmp/auth_keys
+mv /tmp/new_keys /root/.ssh/authorized_keys
+chmod 600 /root/.ssh/authorized_keys
+
+echo "Fetching host keys..."
+WGET -O /tmp/ssh_host_ed25519_key http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh_host_ed25519_key
+WGET -O /tmp/ssh_host_ed25519_key.pub http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh_host_ed25519_key_pub
+mv -f /tmp/ssh_host_ed25519_key* /etc/ssh/
+chmod 600 /etc/ssh/ssh_host_ed25519_key
+chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
index 327324f2921da..b6b1ffa395814 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -69,6 +69,31 @@ in
   # GC has 1460 MTU
   networking.interfaces.eth0.mtu = 1460;
 
+  # Used by NixOps
+  systemd.services.fetch-instance-ssh-keys = {
+    description = "Fetch host keys and authorized_keys for root user";
+
+    wantedBy = [ "sshd.service" ];
+    before = [ "sshd.service" ];
+    after = [ "network-online.target" ];
+    wants = [ "network-online.target" ];
+    path = [ pkgs.wget ];
+
+    serviceConfig = {
+      Type = "oneshot";
+      ExecStart = pkgs.runCommand "fetch-instance-ssh-keys" { } ''
+        cp ${./fetch-instance-ssh-keys.bash} $out
+        chmod +x $out
+        ${pkgs.shfmt}/bin/shfmt -i 4 -d $out
+        ${pkgs.shellcheck}/bin/shellcheck $out
+        patchShebangs $out
+      '';
+      PrivateTmp = true;
+      StandardError = "journal+console";
+      StandardOutput = "journal+console";
+    };
+  };
+
   systemd.services.google-instance-setup = {
     description = "Google Compute Engine Instance Setup";
     after = [ "network-online.target" "network.target" "rsyslog.service" ];
diff --git a/nixos/modules/virtualisation/google-compute-image.nix b/nixos/modules/virtualisation/google-compute-image.nix
index d172ae38fdcfa..e2332df611aa0 100644
--- a/nixos/modules/virtualisation/google-compute-image.nix
+++ b/nixos/modules/virtualisation/google-compute-image.nix
@@ -43,7 +43,7 @@ in
     system.build.googleComputeImage = import ../../lib/make-disk-image.nix {
       name = "google-compute-image";
       postVM = ''
-        PATH=$PATH:${with pkgs; stdenv.lib.makeBinPath [ gnutar gzip ]}
+        PATH=$PATH:${with pkgs; lib.makeBinPath [ gnutar gzip ]}
         pushd $out
         mv $diskImage disk.raw
         tar -Szcf nixos-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.raw.tar.gz disk.raw
diff --git a/nixos/modules/virtualisation/hyperv-guest.nix b/nixos/modules/virtualisation/hyperv-guest.nix
index adc2810a99398..a3656c307f963 100644
--- a/nixos/modules/virtualisation/hyperv-guest.nix
+++ b/nixos/modules/virtualisation/hyperv-guest.nix
@@ -31,6 +31,8 @@ in {
         "hv_balloon" "hv_netvsc" "hv_storvsc" "hv_utils" "hv_vmbus"
       ];
 
+      initrd.availableKernelModules = [ "hyperv_keyboard" ];
+
       kernelParams = [
         "video=hyperv_fb:${cfg.videoMode} elevator=noop"
       ];
@@ -38,8 +40,6 @@ in {
 
     environment.systemPackages = [ config.boot.kernelPackages.hyperv-daemons.bin ];
 
-    security.rngd.enable = false;
-
     # enable hotadding cpu/memory
     services.udev.packages = lib.singleton (pkgs.writeTextFile {
       name = "hyperv-cpu-and-memory-hotadd-udev-rules";
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index d493648401872..e47bd59dc016b 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -11,7 +11,7 @@ with lib;
   users.users.root.initialHashedPassword = mkOverride 150 "";
 
   # Some more help text.
-  services.mingetty.helpLine =
+  services.getty.helpLine =
     ''
 
       Log in as "root" with an empty password.
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index 3958fc2c1d7c5..4b2adf4cc699b 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -5,13 +5,12 @@
 with lib;
 
 let
-
   cfg = config.virtualisation.lxd;
-  zfsCfg = config.boot.zfs;
-
-in
+in {
+  imports = [
+    (mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally")
+  ];
 
-{
   ###### interface
 
   options = {
@@ -51,18 +50,10 @@ in
         '';
       };
 
-      zfsPackage = mkOption {
-        type = types.package;
-        default = with pkgs; if zfsCfg.enableUnstable then zfsUnstable else zfs;
-        defaultText = "pkgs.zfs";
-        description = ''
-          The ZFS package to use with LXD.
-        '';
-      };
-
       zfsSupport = mkOption {
         type = types.bool;
-        default = false;
+        default = config.boot.zfs.enabled;
+        defaultText = "config.boot.zfs.enabled";
         description = ''
           Enables lxd to use zfs as a storage for containers.
 
@@ -87,7 +78,6 @@ in
   };
 
   ###### implementation
-
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
@@ -100,13 +90,17 @@ in
       packages = [ cfg.lxcPackage ];
     };
 
+    # TODO: remove once LXD gets proper support for cgroupsv2
+    # (currently most of the e.g. CPU accounting stuff doesn't work)
+    systemd.enableUnifiedCgroupHierarchy = false;
+
     systemd.services.lxd = {
       description = "LXD Container Management Daemon";
 
       wantedBy = [ "multi-user.target" ];
       after = [ "systemd-udev-settle.service" ];
 
-      path = lib.optional cfg.zfsSupport cfg.zfsPackage;
+      path = lib.optional config.boot.zfs.enabled config.boot.zfs.package;
 
       preStart = ''
         mkdir -m 0755 -p /var/lib/lxc/rootfs
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index 8fbb4efd20196..f06977f88fc16 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -56,10 +56,10 @@ let
             ip -6 route add $HOST_ADDRESS6 dev eth0
             ip -6 route add default via $HOST_ADDRESS6
           fi
-
-          ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
         fi
 
+        ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
+
         # Start the regular stage 1 script.
         exec "$1"
       ''
@@ -170,7 +170,7 @@ let
 
       ${concatStringsSep "\n" (
         mapAttrsToList (name: cfg:
-          ''ip link del dev ${name} 2> /dev/null || true ''
+          "ip link del dev ${name} 2> /dev/null || true "
         ) cfg.extraVeths
       )}
    '';
@@ -185,7 +185,7 @@ let
             fi
           ''
         else
-          ''${ipcmd} add ${cfg.${attribute}} dev $ifaceHost'';
+          "${ipcmd} add ${cfg.${attribute}} dev $ifaceHost";
       renderExtraVeth = name: cfg:
         if cfg.hostBridge != null then
           ''
@@ -223,8 +223,8 @@ let
             ${ipcall cfg "ip route" "$LOCAL_ADDRESS" "localAddress"}
             ${ipcall cfg "ip -6 route" "$LOCAL_ADDRESS6" "localAddress6"}
           fi
-          ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
         fi
+        ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
       ''
   );
 
@@ -463,21 +463,15 @@ in
         { config, options, name, ... }:
         {
           options = {
-
             config = mkOption {
               description = ''
                 A specification of the desired configuration of this
                 container, as a NixOS module.
               '';
-              type = let
-                confPkgs = if config.pkgs == null then pkgs else config.pkgs;
-              in lib.mkOptionType {
+              type = lib.mkOptionType {
                 name = "Toplevel NixOS config";
-                merge = loc: defs: (import (confPkgs.path + "/nixos/lib/eval-config.nix") {
+                merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
                   inherit system;
-                  pkgs = confPkgs;
-                  baseModules = import (confPkgs.path + "/nixos/modules/module-list.nix");
-                  inherit (confPkgs) lib;
                   modules =
                     let
                       extraConfig = {
@@ -526,12 +520,18 @@ in
               '';
             };
 
-            pkgs = mkOption {
-              type = types.nullOr types.attrs;
-              default = null;
-              example = literalExample "pkgs";
+            nixpkgs = mkOption {
+              type = types.path;
+              default = pkgs.path;
+              defaultText = "pkgs.path";
               description = ''
-                Customise which nixpkgs to use for this container.
+                A path to the nixpkgs that provide the modules, pkgs and lib for evaluating the container.
+
+                To only change the <literal>pkgs</literal> argument used inside the container modules,
+                set the <literal>nixpkgs.*</literal> options in the container <option>config</option>.
+                Setting <literal>config.nixpkgs.pkgs = pkgs</literal> speeds up the container evaluation
+                by reusing the system pkgs, but the <literal>nixpkgs.config</literal> option in the
+                container config is ignored in this case.
               '';
             };
 
@@ -614,17 +614,17 @@ in
               '';
             };
 
-		    timeoutStartSec = mkOption {
-		      type = types.str;
-		      default = "1min";
-		      description = ''
-		        Time for the container to start. In case of a timeout,
-		        the container processes get killed.
-		        See <citerefentry><refentrytitle>systemd.time</refentrytitle>
-		        <manvolnum>7</manvolnum></citerefentry>
-		        for more information about the format.
-		       '';
-		    };
+            timeoutStartSec = mkOption {
+              type = types.str;
+              default = "1min";
+              description = ''
+                Time for the container to start. In case of a timeout,
+                the container processes get killed.
+                See <citerefentry><refentrytitle>systemd.time</refentrytitle>
+                <manvolnum>7</manvolnum></citerefentry>
+                for more information about the format.
+               '';
+            };
 
             bindMounts = mkOption {
               type = with types; attrsOf (submodule bindMountOpts);
@@ -672,14 +672,31 @@ in
               '';
             };
 
+            # Removed option. See `checkAssertion` below for the accompanying error message.
+            pkgs = mkOption { visible = false; };
           } // networkOptions;
 
-          config = mkMerge
-            [
-              (mkIf options.config.isDefined {
-                path = config.config.system.build.toplevel;
-              })
-            ];
+          config = let
+            # Throw an error when removed option `pkgs` is used.
+            # Because this is a submodule we cannot use `mkRemovedOptionModule` or option `assertions`.
+            optionPath = "containers.${name}.pkgs";
+            files = showFiles options.pkgs.files;
+            checkAssertion = if options.pkgs.isDefined then throw ''
+              The option definition `${optionPath}' in ${files} no longer has any effect; please remove it.
+
+              Alternatively, you can use the following options:
+              - containers.${name}.nixpkgs
+                This sets the nixpkgs (and thereby the modules, pkgs and lib) that
+                are used for evaluating the container.
+
+              - containers.${name}.config.nixpkgs.pkgs
+                This only sets the `pkgs` argument used inside the container modules.
+            ''
+            else null;
+          in {
+            path = builtins.seq checkAssertion
+              mkIf options.config.isDefined config.config.system.build.toplevel;
+          };
         }));
 
       default = {};
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index a46dd65eb4910..2dd15e3aba455 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -176,10 +176,10 @@ let
           description = ''
             Define which other containers this one depends on. They will be added to both After and Requires for the unit.
 
-            Use the same name as the attribute under <literal>virtualisation.oci-containers</literal>.
+            Use the same name as the attribute under <literal>virtualisation.oci-containers.containers</literal>.
           '';
           example = literalExample ''
-            virtualisation.oci-containers = {
+            virtualisation.oci-containers.containers = {
               node1 = {};
               node2 = {
                 dependsOn = [ "node1" ];
@@ -217,7 +217,7 @@ let
     environment = proxy_env;
 
     path =
-      if cfg.backend == "docker" then [ pkgs.docker ]
+      if cfg.backend == "docker" then [ config.virtualisation.docker.package ]
       else if cfg.backend == "podman" then [ config.virtualisation.podman.package ]
       else throw "Unhandled backend: ${cfg.backend}";
 
@@ -227,29 +227,30 @@ let
         ${cfg.backend} load -i ${container.imageFile}
         ''}
       '';
+
+    script = concatStringsSep " \\\n  " ([
+      "exec ${cfg.backend} run"
+      "--rm"
+      "--name=${escapeShellArg name}"
+      "--log-driver=${container.log-driver}"
+    ] ++ optional (container.entrypoint != null)
+      "--entrypoint=${escapeShellArg container.entrypoint}"
+      ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
+      ++ map (p: "-p ${escapeShellArg p}") container.ports
+      ++ optional (container.user != null) "-u ${escapeShellArg container.user}"
+      ++ map (v: "-v ${escapeShellArg v}") container.volumes
+      ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
+      ++ map escapeShellArg container.extraOptions
+      ++ [container.image]
+      ++ map escapeShellArg container.cmd
+    );
+
+    preStop = "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}";
     postStop = "${cfg.backend} rm -f ${name} || true";
 
     serviceConfig = {
       StandardOutput = "null";
       StandardError = "null";
-      ExecStart = concatStringsSep " \\\n  " ([
-        "${config.system.path}/bin/${cfg.backend} run"
-        "--rm"
-        "--name=${name}"
-        "--log-driver=${container.log-driver}"
-      ] ++ optional (container.entrypoint != null)
-        "--entrypoint=${escapeShellArg container.entrypoint}"
-        ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
-        ++ map (p: "-p ${escapeShellArg p}") container.ports
-        ++ optional (container.user != null) "-u ${escapeShellArg container.user}"
-        ++ map (v: "-v ${escapeShellArg v}") container.volumes
-        ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
-        ++ map escapeShellArg container.extraOptions
-        ++ [container.image]
-        ++ map escapeShellArg container.cmd
-      );
-
-      ExecStop = ''${pkgs.bash}/bin/sh -c "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}"'';
 
       ### There is no generalized way of supporting `reload` for docker
       ### containers. Some containers may respond well to SIGHUP sent to their
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
index c2da5d0d2301e..d01e0f23aba17 100644
--- a/nixos/modules/virtualisation/openstack-config.nix
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  metadataFetcher = import ./ec2-metadata-fetcher.nix {
+  metadataFetcher = import ./openstack-metadata-fetcher.nix {
     targetRoot = "/";
     wgetExtraOptions = "--retry-connrefused";
   };
diff --git a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
new file mode 100644
index 0000000000000..8c191397cf9a5
--- /dev/null
+++ b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
@@ -0,0 +1,21 @@
+{ targetRoot, wgetExtraOptions }:
+
+# OpenStack's metadata service aims to be EC2-compatible. Where
+# possible, try to keep the set of fetched metadata in sync with
+# ./ec2-metadata-fetcher.nix .
+''
+  metaDir=${targetRoot}etc/ec2-metadata
+  mkdir -m 0755 -p "$metaDir"
+  rm -f "$metaDir/*"
+
+  echo "getting instance metadata..."
+
+  wget_imds() {
+    wget ${wgetExtraOptions} "$@"
+  }
+
+  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
+  wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
+  wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
+''
diff --git a/nixos/modules/virtualisation/podman.nix b/nixos/modules/virtualisation/podman.nix
index f554aeffb451d..0223c0df1f222 100644
--- a/nixos/modules/virtualisation/podman.nix
+++ b/nixos/modules/virtualisation/podman.nix
@@ -1,6 +1,7 @@
 { config, lib, pkgs, utils, ... }:
 let
   cfg = config.virtualisation.podman;
+  toml = pkgs.formats.toml { };
 
   inherit (lib) mkOption types;
 
@@ -53,6 +54,14 @@ in
       '';
     };
 
+    enableNvidia = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable use of NVidia GPUs from within podman containers.
+      '';
+    };
+
     extraPackages = mkOption {
       type = with types; listOf package;
       default = [ ];
@@ -78,21 +87,40 @@ in
 
   };
 
-  config = lib.mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ]
-      ++ lib.optional cfg.dockerCompat dockerCompat;
+  config = lib.mkIf cfg.enable (lib.mkMerge [
+    {
+      environment.systemPackages = [ cfg.package ]
+        ++ lib.optional cfg.dockerCompat dockerCompat;
+
+      environment.etc."cni/net.d/87-podman-bridge.conflist".source = utils.copyFile "${pkgs.podman-unwrapped.src}/cni/87-podman-bridge.conflist";
+
+      virtualisation.containers = {
+        enable = true; # Enable common /etc/containers configuration
+        containersConf.extraConfig = lib.optionalString cfg.enableNvidia
+          (builtins.readFile (toml.generate "podman.nvidia.containers.conf" {
+            engine = {
+              conmon_env_vars = [ "PATH=${lib.makeBinPath [ pkgs.nvidia-podman ]}" ];
+              runtimes.nvidia = [ "${pkgs.nvidia-podman}/bin/nvidia-container-runtime" ];
+            };
+          }));
+      };
 
-    environment.etc."cni/net.d/87-podman-bridge.conflist".source = utils.copyFile "${pkgs.podman-unwrapped.src}/cni/87-podman-bridge.conflist";
+      systemd.packages = [ cfg.package ];
 
-    # Enable common /etc/containers configuration
-    virtualisation.containers.enable = true;
+      systemd.services.podman.serviceConfig = {
+        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
+      };
 
-    assertions = [{
-      assertion = cfg.dockerCompat -> !config.virtualisation.docker.enable;
-      message = "Option dockerCompat conflicts with docker";
-    }];
+      systemd.sockets.podman.wantedBy = [ "sockets.target" ];
 
-  };
+      systemd.tmpfiles.packages = [ cfg.package ];
 
+      assertions = [
+        {
+          assertion = cfg.dockerCompat -> !config.virtualisation.docker.enable;
+          message = "Option dockerCompat conflicts with docker";
+        }
+      ];
+    }
+  ]);
 }
diff --git a/nixos/modules/virtualisation/qemu-guest-agent.nix b/nixos/modules/virtualisation/qemu-guest-agent.nix
index 665224e35d8cd..6a735f451a7e3 100644
--- a/nixos/modules/virtualisation/qemu-guest-agent.nix
+++ b/nixos/modules/virtualisation/qemu-guest-agent.nix
@@ -12,6 +12,11 @@ in {
         default = false;
         description = "Whether to enable the qemu guest agent.";
       };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.qemu.ga;
+        description = "The QEMU guest agent package.";
+      };
   };
 
   config = mkIf cfg.enable (
@@ -25,7 +30,7 @@ in {
       systemd.services.qemu-guest-agent = {
         description = "Run the QEMU Guest Agent";
         serviceConfig = {
-          ExecStart = "${pkgs.qemu.ga}/bin/qemu-ga";
+          ExecStart = "${cfg.package}/bin/qemu-ga";
           Restart = "always";
           RestartSec = 0;
         };
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 42e43f5ee0233..5360cff22f3c6 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -7,17 +7,18 @@
 # the VM in the host.  On the other hand, the root filesystem is a
 # read/writable disk image persistent across VM reboots.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, options, ... }:
 
 with lib;
 with import ../../lib/qemu-flags.nix { inherit pkgs; };
 
 let
 
-  qemu = config.system.build.qemu or pkgs.qemu_test;
 
   cfg = config.virtualisation;
 
+  qemu = cfg.qemu.package;
+
   consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
 
   driveOpts = { ... }: {
@@ -135,10 +136,8 @@ let
             cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" || exit 1
             chmod 0644 "$NIX_EFI_VARS" || exit 1
           fi
-        '' else ''
-        ''}
-      '' else ''
-      ''}
+        '' else ""}
+      '' else ""}
 
       cd $TMPDIR
       idx=0
@@ -186,10 +185,9 @@ let
                 efiVars=$out/efi-vars.fd
                 cp ${efiVarsDefault} $efiVars
                 chmod 0644 $efiVars
-              '' else ''
-              ''}
+              '' else ""}
             '';
-          buildInputs = [ pkgs.utillinux ];
+          buildInputs = [ pkgs.util-linux ];
           QEMU_OPTS = "-nographic -serial stdio -monitor none"
                       + lib.optionalString cfg.useEFIBoot (
                         " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
@@ -268,6 +266,8 @@ in
 
   options = {
 
+    virtualisation.fileSystems = options.fileSystems;
+
     virtualisation.memorySize =
       mkOption {
         default = 384;
@@ -401,6 +401,14 @@ in
       };
 
     virtualisation.qemu = {
+      package =
+        mkOption {
+          type = types.package;
+          default = pkgs.qemu;
+          example = "pkgs.qemu_test";
+          description = "QEMU package to use.";
+        };
+
       options =
         mkOption {
           type = types.listOf types.unspecified;
@@ -653,6 +661,7 @@ in
     # attribute should be disregarded for the purpose of building a VM
     # test image (since those filesystems don't exist in the VM).
     fileSystems = mkVMOverride (
+      cfg.fileSystems //
       { "/".device = cfg.bootDevice;
         ${if cfg.writableStore then "/nix/.ro-store" else "/nix/store"} =
           { device = "store";
@@ -735,16 +744,19 @@ in
         (isEnabled "VIRTIO_PCI")
         (isEnabled "VIRTIO_NET")
         (isEnabled "EXT4_FS")
+        (isEnabled "NET_9P_VIRTIO")
+        (isEnabled "9P_FS")
         (isYes "BLK_DEV")
         (isYes "PCI")
-        (isYes "EXPERIMENTAL")
         (isYes "NETDEVICES")
         (isYes "NET_CORE")
         (isYes "INET")
         (isYes "NETWORK_FILESYSTEMS")
-      ] ++ optional (!cfg.graphics) [
+      ] ++ optionals (!cfg.graphics) [
         (isYes "SERIAL_8250_CONSOLE")
         (isYes "SERIAL_8250")
+      ] ++ optionals (cfg.writableStore) [
+        (isEnabled "OVERLAY_FS")
       ];
 
   };
diff --git a/nixos/modules/virtualisation/railcar.nix b/nixos/modules/virtualisation/railcar.nix
index 10464f6289846..b603effef6e01 100644
--- a/nixos/modules/virtualisation/railcar.nix
+++ b/nixos/modules/virtualisation/railcar.nix
@@ -105,7 +105,7 @@ in
 
     stateDir = mkOption {
       type = types.path;
-      default = ''/var/railcar'';
+      default = "/var/railcar";
       description = "Railcar persistent state directory";
     };
 
diff --git a/nixos/modules/virtualisation/vagrant-guest.nix b/nixos/modules/virtualisation/vagrant-guest.nix
new file mode 100644
index 0000000000000..263b1ebca0868
--- /dev/null
+++ b/nixos/modules/virtualisation/vagrant-guest.nix
@@ -0,0 +1,58 @@
+# Minimal configuration that vagrant depends on
+
+{ config, pkgs, ... }:
+let
+  # Vagrant uses an insecure shared private key by default, but we
+  # don't use the authorizedKeys attribute under users because it should be
+  # removed on first boot and replaced with a random one. This script sets
+  # the correct permissions and installs the temporary key if no
+  # ~/.ssh/authorized_keys exists.
+  install-vagrant-ssh-key = pkgs.writeScriptBin "install-vagrant-ssh-key" ''
+    #!${pkgs.runtimeShell}
+    if [ ! -e ~/.ssh/authorized_keys ]; then
+      mkdir -m 0700 -p ~/.ssh
+      echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key" >> ~/.ssh/authorized_keys
+      chmod 0600 ~/.ssh/authorized_keys
+    fi
+  '';
+in
+{
+  # Enable the OpenSSH daemon.
+  services.openssh.enable = true;
+
+  # Packages used by Vagrant
+  environment.systemPackages = with pkgs; [
+    findutils
+    iputils
+    nettools
+    netcat
+    nfs-utils
+    rsync
+  ];
+
+  users.extraUsers.vagrant = {
+    isNormalUser    = true;
+    createHome      = true;
+    description     = "Vagrant user account";
+    extraGroups     = [ "users" "wheel" ];
+    home            = "/home/vagrant";
+    password        = "vagrant";
+    useDefaultShell = true;
+    uid             = 1000;
+  };
+
+  systemd.services.install-vagrant-ssh-key = {
+    description = "Vagrant SSH key install (if needed)";
+    after = [ "fs.target" ];
+    wants = [ "fs.target" ];
+    wantedBy = [ "multi-user.target" ];
+    serviceConfig = {
+      ExecStart = "${install-vagrant-ssh-key}/bin/install-vagrant-ssh-key";
+      User = "vagrant";
+      # So it won't be (needlessly) restarted:
+      RemainAfterExit = true;
+    };
+  };
+
+  security.sudo.wheelNeedsPassword = false;
+}
diff --git a/nixos/modules/virtualisation/vagrant-virtualbox-image.nix b/nixos/modules/virtualisation/vagrant-virtualbox-image.nix
new file mode 100644
index 0000000000000..2a921894ab612
--- /dev/null
+++ b/nixos/modules/virtualisation/vagrant-virtualbox-image.nix
@@ -0,0 +1,60 @@
+# Vagrant + VirtualBox
+
+{ config, pkgs, ... }:
+
+{
+  imports = [
+    ./vagrant-guest.nix
+    ./virtualbox-image.nix
+  ];
+
+  virtualbox.params = {
+    audio = "none";
+    audioin = "off";
+    audioout = "off";
+    usb = "off";
+    usbehci = "off";
+  };
+  sound.enable = false;
+  documentation.man.enable = false;
+  documentation.nixos.enable = false;
+
+  users.extraUsers.vagrant.extraGroups = [ "vboxsf" ];
+
+  # generate the box v1 format which is much easier to generate
+  # https://www.vagrantup.com/docs/boxes/format.html
+  system.build.vagrantVirtualbox = pkgs.runCommand
+    "virtualbox-vagrant.box"
+    {}
+    ''
+      mkdir workdir
+      cd workdir
+
+      # 1. create that metadata.json file
+      echo '{"provider":"virtualbox"}' > metadata.json
+
+      # 2. create a default Vagrantfile config
+      cat <<VAGRANTFILE > Vagrantfile
+      Vagrant.configure("2") do |config|
+        config.vm.base_mac = "0800275F0936"
+      end
+      VAGRANTFILE
+
+      # 3. add the exported VM files
+      tar xvf ${config.system.build.virtualBoxOVA}/*.ova
+
+      # 4. move the ovf to the fixed location
+      mv *.ovf box.ovf
+
+      # 5. generate OVF manifest file
+      rm *.mf
+      touch box.mf
+      for fname in *; do
+        checksum=$(sha256sum $fname | cut -d' ' -f 1)
+        echo "SHA256($fname)= $checksum" >> box.mf
+      done
+
+      # 6. compress everything back together
+      tar --owner=0 --group=0 --sort=name --numeric-owner -czf $out .
+    '';
+}
diff --git a/nixos/modules/virtualisation/xen-dom0.nix b/nixos/modules/virtualisation/xen-dom0.nix
index 7b2a66c43489c..5b57ca860da29 100644
--- a/nixos/modules/virtualisation/xen-dom0.nix
+++ b/nixos/modules/virtualisation/xen-dom0.nix
@@ -57,7 +57,8 @@ in
 
     virtualisation.xen.bootParams =
       mkOption {
-        default = "";
+        default = [];
+        type = types.listOf types.str;
         description =
           ''
             Parameters passed to the Xen hypervisor at boot time.
@@ -68,6 +69,7 @@ in
       mkOption {
         default = 0;
         example = 512;
+        type = types.addCheck types.int (n: n >= 0);
         description =
           ''
             Amount of memory (in MiB) allocated to Domain 0 on boot.
@@ -78,6 +80,7 @@ in
     virtualisation.xen.bridge = {
         name = mkOption {
           default = "xenbr0";
+          type = types.str;
           description = ''
               Name of bridge the Xen domUs connect to.
             '';
@@ -201,8 +204,8 @@ in
       ''
         if [ -d /proc/xen ]; then
             ${pkgs.kmod}/bin/modprobe xenfs 2> /dev/null
-            ${pkgs.utillinux}/bin/mountpoint -q /proc/xen || \
-                ${pkgs.utillinux}/bin/mount -t xenfs none /proc/xen
+            ${pkgs.util-linux}/bin/mountpoint -q /proc/xen || \
+                ${pkgs.util-linux}/bin/mount -t xenfs none /proc/xen
         fi
       '';
 
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index 945ba90e3454c..ea82adf09ad1d 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -49,6 +49,7 @@ in rec {
         [ "nixos.channel" ]
         (onFullSupported "nixos.dummy")
         (onAllSupported "nixos.iso_minimal")
+        (onSystems ["x86_64-linux" "aarch64-linux"] "nixos.amazonImage")
         (onSystems ["x86_64-linux"] "nixos.iso_plasma5")
         (onSystems ["x86_64-linux"] "nixos.iso_gnome")
         (onFullSupported "nixos.manual")
@@ -71,7 +72,6 @@ in rec {
         (onFullSupported "nixos.tests.fontconfig-default-fonts")
         (onFullSupported "nixos.tests.gnome3")
         (onFullSupported "nixos.tests.gnome3-xorg")
-        (onFullSupported "nixos.tests.hardened")
         (onSystems ["x86_64-linux"] "nixos.tests.hibernate")
         (onFullSupported "nixos.tests.i3wm")
         (onSystems ["x86_64-linux"] "nixos.tests.installer.btrfsSimple")
@@ -93,12 +93,12 @@ in rec {
         (onFullSupported "nixos.tests.keymap.dvp")
         (onFullSupported "nixos.tests.keymap.neo")
         (onFullSupported "nixos.tests.keymap.qwertz")
-        (onFullSupported "nixos.tests.latestKernel.hardened")
         (onFullSupported "nixos.tests.latestKernel.login")
         (onFullSupported "nixos.tests.lightdm")
         (onFullSupported "nixos.tests.login")
         (onFullSupported "nixos.tests.misc")
         (onFullSupported "nixos.tests.mutableUsers")
+        (onFullSupported "nixos.tests.nano")
         (onFullSupported "nixos.tests.nat.firewall-conntrack")
         (onFullSupported "nixos.tests.nat.firewall")
         (onFullSupported "nixos.tests.nat.standalone")
@@ -145,6 +145,7 @@ in rec {
         (onFullSupported "nixos.tests.printing")
         (onFullSupported "nixos.tests.proxy")
         (onFullSupported "nixos.tests.sddm.default")
+        (onFullSupported "nixos.tests.shadow")
         (onFullSupported "nixos.tests.simple")
         (onFullSupported "nixos.tests.switchTest")
         (onFullSupported "nixos.tests.udisks2")
diff --git a/nixos/release-small.nix b/nixos/release-small.nix
index 6da2c59cedd49..996db54c9a402 100644
--- a/nixos/release-small.nix
+++ b/nixos/release-small.nix
@@ -28,7 +28,7 @@ let
 in rec {
 
   nixos = {
-    inherit (nixos') channel manual options iso_minimal dummy;
+    inherit (nixos') channel manual options iso_minimal amazonImage dummy;
     tests = {
       inherit (nixos'.tests)
         containers-imperative
@@ -92,6 +92,7 @@ in rec {
       [ "nixos.channel"
         "nixos.dummy.x86_64-linux"
         "nixos.iso_minimal.x86_64-linux"
+        "nixos.amazonImage.x86_64-linux"
         "nixos.manual.x86_64-linux"
         "nixos.tests.boot.biosCdrom.x86_64-linux"
         "nixos.tests.containers-imperative.x86_64-linux"
diff --git a/nixos/release.nix b/nixos/release.nix
index 1f5c158126955..109747945f78a 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -79,7 +79,7 @@ let
     in
       tarball //
         { meta = {
-            description = "NixOS system tarball for ${system} - ${stdenv.hostPlatform.platform.name}";
+            description = "NixOS system tarball for ${system} - ${stdenv.hostPlatform.linux-kernel.name}";
             maintainers = map (x: lib.maintainers.${x}) maintainers;
           };
           inherit config;
@@ -105,7 +105,7 @@ let
         modules = makeModules module {};
       };
       build = configEvaled.config.system.build;
-      kernelTarget = configEvaled.pkgs.stdenv.hostPlatform.platform.kernelTarget;
+      kernelTarget = configEvaled.pkgs.stdenv.hostPlatform.linux-kernel.target;
     in
       pkgs.symlinkJoin {
         name = "netboot";
diff --git a/nixos/tests/3proxy.nix b/nixos/tests/3proxy.nix
index de3056f6710f5..dfc4b35a772dc 100644
--- a/nixos/tests/3proxy.nix
+++ b/nixos/tests/3proxy.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "3proxy";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ misuzu ];
   };
 
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 64193ed8498cb..c6d393d919639 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -77,6 +77,27 @@ in import ./make-test-python.nix ({ lib, ... }: {
         after = [ "acme-a.example.test.service" "nginx-config-reload.service" ];
       };
 
+      # Test that account creation is collated into one service
+      specialisation.account-creation.configuration = { nodes, pkgs, lib, ... }: let
+        email = "newhostmaster@example.test";
+        caDomain = nodes.acme.config.test-support.acme.caDomain;
+        # Exit 99 to make it easier to track if this is the reason a renew failed
+        testScript = ''
+          test -e accounts/${caDomain}/${email}/account.json || exit 99
+        '';
+      in {
+        security.acme.email = lib.mkForce email;
+        systemd.services."b.example.test".preStart = testScript;
+        systemd.services."c.example.test".preStart = testScript;
+
+        services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
+          enableACME = true;
+        };
+        services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
+          enableACME = true;
+        };
+      };
+
       # Cert config changes will not cause the nginx configuration to change.
       # This tests that the reload service is correctly triggered.
       # It also tests that postRun is exec'd as root
@@ -97,6 +118,19 @@ in import ./make-test-python.nix ({ lib, ... }: {
         };
       };
 
+      # Test OCSP Stapling
+      specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
+        security.acme.certs."a.example.test" = {
+          ocspMustStaple = true;
+        };
+        services.nginx.virtualHosts."a.example.com" = {
+          extraConfig = ''
+            ssl_stapling on;
+            ssl_stapling_verify on;
+          '';
+        };
+      };
+
       # Test using Apache HTTPD
       specialisation.httpd-aliases.configuration = { pkgs, config, lib, ... }: {
         services.nginx.enable = lib.mkForce false;
@@ -163,6 +197,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
 
   testScript = {nodes, ...}:
     let
+      caDomain = nodes.acme.config.test-support.acme.caDomain;
       newServerSystem = nodes.webserver.config.system.build.toplevel;
       switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
     in
@@ -246,6 +281,22 @@ in import ./make-test-python.nix ({ lib, ... }: {
               return check_connection_key_bits(node, domain, bits, retries - 1)
 
 
+      def check_stapling(node, domain, retries=3):
+          assert retries >= 0
+
+          # Pebble doesn't provide a full OCSP responder, so just check the URL
+          result = node.succeed(
+              "openssl s_client -CAfile /tmp/ca.crt"
+              f" -servername {domain} -connect {domain}:443 < /dev/null"
+              " | openssl x509 -noout -ocsp_uri"
+          )
+          print("OCSP Responder URL:", result)
+
+          if "${caDomain}:4002" not in result.lower():
+              time.sleep(1)
+              return check_stapling(node, domain, retries - 1)
+
+
       client.start()
       dnsserver.start()
 
@@ -253,17 +304,17 @@ in import ./make-test-python.nix ({ lib, ... }: {
       client.wait_for_unit("default.target")
 
       client.succeed(
-          'curl --data \'{"host": "acme.test", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
+          'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
       )
 
       acme.start()
       webserver.start()
 
-      acme.wait_for_unit("default.target")
+      acme.wait_for_unit("network-online.target")
       acme.wait_for_unit("pebble.service")
 
-      client.succeed("curl https://acme.test:15000/roots/0 > /tmp/ca.crt")
-      client.succeed("curl https://acme.test:15000/intermediate-keys/0 >> /tmp/ca.crt")
+      client.succeed("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt")
+      client.succeed("curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt")
 
       with subtest("Can request certificate with HTTPS-01 challenge"):
           webserver.wait_for_unit("acme-finished-a.example.test.target")
@@ -284,12 +335,26 @@ in import ./make-test-python.nix ({ lib, ... }: {
           check_issuer(webserver, "a.example.test", "pebble")
           check_connection(client, "a.example.test")
 
+      with subtest("Runs 1 cert for account creation before others"):
+          switch_to(webserver, "account-creation")
+          webserver.wait_for_unit("acme-finished-a.example.test.target")
+          check_connection(client, "a.example.test")
+          webserver.wait_for_unit("acme-finished-b.example.test.target")
+          webserver.wait_for_unit("acme-finished-c.example.test.target")
+          check_connection(client, "b.example.test")
+          check_connection(client, "c.example.test")
+
       with subtest("Can reload web server when cert configuration changes"):
           switch_to(webserver, "cert-change")
           webserver.wait_for_unit("acme-finished-a.example.test.target")
           check_connection_key_bits(client, "a.example.test", "384")
           webserver.succeed("grep testing /var/lib/acme/a.example.test/test")
 
+      with subtest("Correctly implements OCSP stapling"):
+          switch_to(webserver, "ocsp-stapling")
+          webserver.wait_for_unit("acme-finished-a.example.test.target")
+          check_stapling(client, "a.example.test")
+
       with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"):
           switch_to(webserver, "slow-startup")
           webserver.wait_for_unit("acme-finished-slow.example.com.target")
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
index 3b3eb2803bdd3..3773907cff557 100644
--- a/nixos/tests/agda.nix
+++ b/nixos/tests/agda.nix
@@ -9,7 +9,7 @@ let
 in
 {
   name = "agda";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ alexarice turion ];
   };
 
@@ -23,6 +23,13 @@ in
   };
 
   testScript = ''
+    assert (
+        "${pkgs.agdaPackages.lib.interfaceFile "Everything.agda"}" == "Everything.agdai"
+    ), "wrong interface file for Everything.agda"
+    assert (
+        "${pkgs.agdaPackages.lib.interfaceFile "tmp/Everything.agda.md"}" == "tmp/Everything.agdai"
+    ), "wrong interface file for tmp/Everything.agda.md"
+
     # Minimal script that typechecks
     machine.succeed("touch TestEmpty.agda")
     machine.succeed("agda TestEmpty.agda")
@@ -36,6 +43,10 @@ in
         "cp ${hello-world} HelloWorld.agda"
     )
     machine.succeed("agda -l standard-library -i . -c HelloWorld.agda")
+    # Check execution
+    assert "Hello World!" in machine.succeed(
+        "./HelloWorld"
+    ), "HelloWorld does not run properly"
   '';
 }
 )
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 0dc14e527d125..89e2ee2722847 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -24,9 +24,13 @@ in
   _3proxy = handleTest ./3proxy.nix {};
   acme = handleTest ./acme.nix {};
   agda = handleTest ./agda.nix {};
+  ammonite = handleTest ./ammonite.nix {};
   atd = handleTest ./atd.nix {};
   avahi = handleTest ./avahi.nix {};
+  avahi-with-resolved = handleTest ./avahi.nix { networkd = true; };
+  awscli = handleTest ./awscli.nix { };
   babeld = handleTest ./babeld.nix {};
+  bat = handleTest ./bat.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 {};
@@ -36,18 +40,21 @@ in
   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 {};
   borgbackup = handleTest ./borgbackup.nix {};
   buildbot = handleTest ./buildbot.nix {};
+  buildkite-agents = handleTest ./buildkite-agents.nix {};
   caddy = handleTest ./caddy.nix {};
   cadvisor = handleTestOn ["x86_64-linux"] ./cadvisor.nix {};
   cage = handleTest ./cage.nix {};
   cagebreak = handleTest ./cagebreak.nix {};
-  cassandra = handleTest ./cassandra.nix {};
-  ceph-single-node = handleTestOn ["x86_64-linux"] ./ceph-single-node.nix {};
+  cassandra_2_1 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_1; };
+  cassandra_2_2 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_2; };
+  cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; };
+  cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; };
   ceph-multi-node = handleTestOn ["x86_64-linux"] ./ceph-multi-node.nix {};
+  ceph-single-node = handleTestOn ["x86_64-linux"] ./ceph-single-node.nix {};
   certmgr = handleTest ./certmgr.nix {};
   cfssl = handleTestOn ["x86_64-linux"] ./cfssl.nix {};
   charliecloud = handleTest ./charliecloud.nix {};
@@ -55,9 +62,8 @@ in
   cjdns = handleTest ./cjdns.nix {};
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
-  codimd = handleTest ./codimd.nix {};
-  consul = handleTest ./consul.nix {};
   cockroachdb = handleTestOn ["x86_64-linux"] ./cockroachdb.nix {};
+  consul = handleTest ./consul.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-custom-pkgs.nix = handleTest ./containers-custom-pkgs.nix {};
   containers-ephemeral = handleTest ./containers-ephemeral.nix {};
@@ -75,16 +81,17 @@ in
   corerad = handleTest ./corerad.nix {};
   couchdb = handleTest ./couchdb.nix {};
   cri-o = handleTestOn ["x86_64-linux"] ./cri-o.nix {};
+  custom-ca = handleTest ./custom-ca.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-registry = handleTest ./docker-registry.nix {};
   docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
+  docker-tools-cross = handleTestOn ["x86_64-linux" "aarch64-linux"] ./docker-tools-cross.nix {};
   docker-tools-overlay = handleTestOn ["x86_64-linux"] ./docker-tools-overlay.nix {};
   documize = handleTest ./documize.nix {};
   dokuwiki = handleTest ./dokuwiki.nix {};
@@ -100,7 +107,9 @@ in
   ergo = handleTest ./ergo.nix {};
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
   etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
+  etesync-dav = handleTest ./etesync-dav.nix {};
   fancontrol = handleTest ./fancontrol.nix {};
+  fcitx = handleTest ./fcitx {};
   ferm = handleTest ./ferm.nix {};
   firefox = handleTest ./firefox.nix {};
   firefox-esr = handleTest ./firefox.nix { esr = true; };
@@ -112,25 +121,25 @@ in
   fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
   freeswitch = handleTest ./freeswitch.nix {};
   fsck = handleTest ./fsck.nix {};
+  ft2-clone = handleTest ./ft2-clone.nix {};
   gerrit = handleTest ./gerrit.nix {};
-  gotify-server = handleTest ./gotify-server.nix {};
-  grocy = handleTest ./grocy.nix {};
   gitdaemon = handleTest ./gitdaemon.nix {};
   gitea = handleTest ./gitea.nix {};
   gitlab = handleTest ./gitlab.nix {};
   gitolite = handleTest ./gitolite.nix {};
   gitolite-fcgiwrap = handleTest ./gitolite-fcgiwrap.nix {};
   glusterfs = handleTest ./glusterfs.nix {};
-  gnome3-xorg = handleTest ./gnome3-xorg.nix {};
   gnome3 = handleTest ./gnome3.nix {};
-  installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
+  gnome3-xorg = handleTest ./gnome3-xorg.nix {};
+  go-neb = handleTest ./go-neb.nix {};
   gocd-agent = handleTest ./gocd-agent.nix {};
   gocd-server = handleTest ./gocd-server.nix {};
-  go-neb = handleTest ./go-neb.nix {};
   google-oslogin = handleTest ./google-oslogin {};
+  gotify-server = handleTest ./gotify-server.nix {};
   grafana = handleTest ./grafana.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
+  grocy = handleTest ./grocy.nix {};
   grub = handleTest ./grub.nix {};
   gvisor = handleTest ./gvisor.nix {};
   hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
@@ -138,26 +147,32 @@ in
   handbrake = handleTestOn ["x86_64-linux"] ./handbrake.nix {};
   haproxy = handleTest ./haproxy.nix {};
   hardened = handleTest ./hardened.nix {};
+  hedgedoc = handleTest ./hedgedoc.nix {};
+  herbstluftwm = handleTest ./herbstluftwm.nix {};
+  installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
+  oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {};
   # 9pnet_virtio used to mount /nix partition doesn't support
   # hibernation. This test happens to work on x86_64-linux but
   # not on other platforms.
   hibernate = handleTestOn ["x86_64-linux"] ./hibernate.nix {};
   hitch = handleTest ./hitch {};
+  hledger-web = handleTest ./hledger-web.nix {};
   hocker-fetchdocker = handleTest ./hocker-fetchdocker {};
   home-assistant = handleTest ./home-assistant.nix {};
   hostname = handleTest ./hostname.nix {};
   hound = handleTest ./hound.nix {};
+  hub = handleTest ./git/hub.nix {};
   hydra = handleTest ./hydra {};
-  hydra-db-migration = handleTest ./hydra/db-migration.nix {};
   i3wm = handleTest ./i3wm.nix {};
   icingaweb2 = handleTest ./icingaweb2.nix {};
   iftop = handleTest ./iftop.nix {};
   ihatemoney = handleTest ./ihatemoney.nix {};
   incron = handleTest ./incron.nix {};
   influxdb = handleTest ./influxdb.nix {};
-  initrd-network-ssh = handleTest ./initrd-network-ssh {};
   initrd-network-openvpn = handleTest ./initrd-network-openvpn {};
+  initrd-network-ssh = handleTest ./initrd-network-ssh {};
   initrdNetwork = handleTest ./initrd-network.nix {};
+  initrd-secrets = handleTest ./initrd-secrets.nix {};
   installer = handleTest ./installer.nix {};
   iodine = handleTest ./iodine.nix {};
   ipfs = handleTest ./ipfs.nix {};
@@ -167,13 +182,17 @@ in
   jenkins = handleTest ./jenkins.nix {};
   jirafeau = handleTest ./jirafeau.nix {};
   jitsi-meet = handleTest ./jitsi-meet.nix {};
+  jq = handleTest ./jq.nix {};
   k3s = handleTest ./k3s.nix {};
   kafka = handleTest ./kafka.nix {};
   keepalived = handleTest ./keepalived.nix {};
+  keepassxc = handleTest ./keepassxc.nix {};
   kerberos = handleTest ./kerberos/default.nix {};
   kernel-latest = handleTest ./kernel-latest.nix {};
   kernel-lts = handleTest ./kernel-lts.nix {};
   kernel-testing = handleTest ./kernel-testing.nix {};
+  kernel-latest-ath-user-regd = handleTest ./kernel-latest-ath-user-regd.nix {};
+  keycloak = discoverTests (import ./keycloak.nix);
   keymap = handleTest ./keymap.nix {};
   knot = handleTest ./knot.nix {};
   krb5 = discoverTests (import ./krb5 {});
@@ -187,15 +206,18 @@ in
   lidarr = handleTest ./lidarr.nix {};
   lightdm = handleTest ./lightdm.nix {};
   limesurvey = handleTest ./limesurvey.nix {};
+  locate = handleTest ./locate.nix {};
   login = handleTest ./login.nix {};
   loki = handleTest ./loki.nix {};
+  lsd = handleTest ./lsd.nix {};
   lxd = handleTest ./lxd.nix {};
   lxd-nftables = handleTest ./lxd-nftables.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
-  magnetico = handleTest ./magnetico.nix {};
   magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
+  magnetico = handleTest ./magnetico.nix {};
   mailcatcher = handleTest ./mailcatcher.nix {};
+  mailhog = handleTest ./mailhog.nix {};
   mariadb-galera-mariabackup = handleTest ./mysql/mariadb-galera-mariabackup.nix {};
   mariadb-galera-rsync = handleTest ./mysql/mariadb-galera-rsync.nix {};
   matomo = handleTest ./matomo.nix {};
@@ -203,9 +225,11 @@ in
   mediawiki = handleTest ./mediawiki.nix {};
   memcached = handleTest ./memcached.nix {};
   metabase = handleTest ./metabase.nix {};
+  minecraft = handleTest ./minecraft.nix {};
+  minecraft-server = handleTest ./minecraft-server.nix {};
+  minidlna = handleTest ./minidlna.nix {};
   miniflux = handleTest ./miniflux.nix {};
   minio = handleTest ./minio.nix {};
-  minidlna = handleTest ./minidlna.nix {};
   misc = handleTest ./misc.nix {};
   moinmoin = handleTest ./moinmoin.nix {};
   mongodb = handleTest ./mongodb.nix {};
@@ -221,17 +245,20 @@ in
   mysql-autobackup = handleTest ./mysql/mysql-autobackup.nix {};
   mysql-backup = handleTest ./mysql/mysql-backup.nix {};
   mysql-replication = handleTest ./mysql/mysql-replication.nix {};
+  n8n = handleTest ./n8n.nix {};
   nagios = handleTest ./nagios.nix {};
+  nano = handleTest ./nano.nix {};
+  nar-serve = handleTest ./nar-serve.nix {};
   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 {};
   netdata = handleTest ./netdata.nix {};
   networking.networkd = handleTest ./networking.nix { networkd = true; };
   networking.scripted = handleTest ./networking.nix { networkd = false; };
+  specialisation = handleTest ./specialisation.nix {};
   # TODO: put in networking.nix after the test becomes more complete
   networkingProxy = handleTest ./networking-proxy.nix {};
   nextcloud = handleTest ./nextcloud {};
@@ -241,6 +268,7 @@ in
   nfs4 = handleTest ./nfs { version = 4; };
   nghttpx = handleTest ./nghttpx.nix {};
   nginx = handleTest ./nginx.nix {};
+  nginx-auth = handleTest ./nginx-auth.nix {};
   nginx-etag = handleTest ./nginx-etag.nix {};
   nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
   nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
@@ -248,15 +276,20 @@ in
   nginx-variants = handleTest ./nginx-variants.nix {};
   nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
   nixos-generate-config = handleTest ./nixos-generate-config.nix {};
+  nomad = handleTest ./nomad.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nsd = handleTest ./nsd.nix {};
   nzbget = handleTest ./nzbget.nix {};
+  nzbhydra2 = handleTest ./nzbhydra2.nix {};
+  oh-my-zsh = handleTest ./oh-my-zsh.nix {};
   openarena = handleTest ./openarena.nix {};
   openldap = handleTest ./openldap.nix {};
   opensmtpd = handleTest ./opensmtpd.nix {};
   openssh = handleTest ./openssh.nix {};
-  openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
   openstack-image-metadata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).metadata or {};
+  openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
+  opentabletdriver = handleTest ./opentabletdriver.nix {};
+  image-contents = handleTest ./image-contents.nix {};
   orangefs = handleTest ./orangefs.nix {};
   os-prober = handleTestOn ["x86_64-linux"] ./os-prober.nix {};
   osrm-backend = handleTest ./osrm-backend.nix {};
@@ -266,12 +299,14 @@ in
   pam-u2f = handleTest ./pam-u2f.nix {};
   pantheon = handleTest ./pantheon.nix {};
   paperless = handleTest ./paperless.nix {};
+  pdns-recursor = handleTest ./pdns-recursor.nix {};
   peerflix = handleTest ./peerflix.nix {};
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   php = handleTest ./php {};
   pinnwand = handleTest ./pinnwand.nix {};
   plasma5 = handleTest ./plasma5.nix {};
+  pleroma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./pleroma.nix {};
   plotinus = handleTest ./plotinus.nix {};
   podman = handleTestOn ["x86_64-linux"] ./podman.nix {};
   postfix = handleTest ./postfix.nix {};
@@ -307,9 +342,14 @@ in
   runInMachine = handleTest ./run-in-machine.nix {};
   rxe = handleTest ./rxe.nix {};
   samba = handleTest ./samba.nix {};
+  samba-wsdd = handleTest ./samba-wsdd.nix {};
   sanoid = handleTest ./sanoid.nix {};
+  sbt = handleTest ./sbt.nix {};
+  sbt-extras = handleTest ./sbt-extras.nix {};
   sddm = handleTest ./sddm.nix {};
+  searx = handleTest ./searx.nix {};
   service-runner = handleTest ./service-runner.nix {};
+  shadow = handleTest ./shadow.nix {};
   shadowsocks = handleTest ./shadowsocks {};
   shattered-pixel-dungeon = handleTest ./shattered-pixel-dungeon.nix {};
   shiori = handleTest ./shiori.nix {};
@@ -321,9 +361,9 @@ in
   snapper = handleTest ./snapper.nix {};
   sogo = handleTest ./sogo.nix {};
   solr = handleTest ./solr.nix {};
+  sonarr = handleTest ./sonarr.nix {};
   spacecookie = handleTest ./spacecookie.nix {};
   spike = handleTest ./spike.nix {};
-  sonarr = handleTest ./sonarr.nix {};
   sslh = handleTest ./sslh.nix {};
   sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {};
   sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {};
@@ -339,35 +379,42 @@ in
   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 {};
+  systemd-journal = handleTest ./systemd-journal.nix {};
   systemd-networkd = handleTest ./systemd-networkd.nix {};
   systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {};
   systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {};
+  systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
-  pdns-recursor = handleTest ./pdns-recursor.nix {};
+  systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
   taskserver = handleTest ./taskserver.nix {};
   telegraf = handleTest ./telegraf.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
   timezone = handleTest ./timezone.nix {};
+  tinc = handleTest ./tinc {};
   tinydns = handleTest ./tinydns.nix {};
   tor = handleTest ./tor.nix {};
   # traefik test relies on docker-containers
+  trac = handleTest ./trac.nix {};
   traefik = handleTestOn ["x86_64-linux"] ./traefik.nix {};
   transmission = handleTest ./transmission.nix {};
-  trac = handleTest ./trac.nix {};
-  trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {};
   trezord = handleTest ./trezord.nix {};
   trickster = handleTest ./trickster.nix {};
+  trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {};
   tuptime = handleTest ./tuptime.nix {};
+  ucg = handleTest ./ucg.nix {};
   udisks2 = handleTest ./udisks2.nix {};
+  unbound = handleTest ./unbound.nix {};
   unit-php = handleTest ./web-servers/unit-php.nix {};
   upnp = handleTest ./upnp.nix {};
+  usbguard = handleTest ./usbguard.nix {};
   uwsgi = handleTest ./uwsgi.nix {};
   v2ray = handleTest ./v2ray.nix {};
   vault = handleTest ./vault.nix {};
+  vault-postgresql = handleTest ./vault-postgresql.nix {};
+  vector = handleTest ./vector.nix {};
   victoriametrics = handleTest ./victoriametrics.nix {};
   virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
+  vscodium = handleTest ./vscodium.nix {};
   wasabibackend = handleTest ./wasabibackend.nix {};
   wireguard = handleTest ./wireguard {};
   wordpress = handleTest ./wordpress.nix {};
@@ -377,8 +424,10 @@ in
   xmonad = handleTest ./xmonad.nix {};
   xrdp = handleTest ./xrdp.nix {};
   xss-lock = handleTest ./xss-lock.nix {};
+  xterm = handleTest ./xterm.nix {};
   yabar = handleTest ./yabar.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
+  yq = handleTest ./yq.nix {};
   zfs = handleTest ./zfs.nix {};
   zigbee2mqtt = handleTest ./zigbee2mqtt.nix {};
   zoneminder = handleTest ./zoneminder.nix {};
diff --git a/nixos/tests/ammonite.nix b/nixos/tests/ammonite.nix
index 1955e42be5f02..4b674f35e3cb0 100644
--- a/nixos/tests/ammonite.nix
+++ b/nixos/tests/ammonite.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "ammonite";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
@@ -8,7 +8,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     amm =
       { pkgs, ... }:
         {
-          environment.systemPackages = [ pkgs.ammonite ];
+          environment.systemPackages = [ (pkgs.ammonite.override { jre = pkgs.jre8; }) ];
         };
     };
 
diff --git a/nixos/tests/atd.nix b/nixos/tests/atd.nix
index c3abe5c253df6..ad4d60067cf1e 100644
--- a/nixos/tests/atd.nix
+++ b/nixos/tests/atd.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "atd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bjornfor ];
   };
 
diff --git a/nixos/tests/avahi.nix b/nixos/tests/avahi.nix
index fe027c14d5a89..ebb46838325f3 100644
--- a/nixos/tests/avahi.nix
+++ b/nixos/tests/avahi.nix
@@ -1,7 +1,14 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+# bool: whether to use networkd in the tests
+, networkd ? false
+} @ args:
+
 # Test whether `avahi-daemon' and `libnss-mdns' work as expected.
-import ./make-test-python.nix ({ pkgs, ... } : {
+import ./make-test-python.nix {
   name = "avahi";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
@@ -17,6 +24,11 @@ import ./make-test-python.nix ({ pkgs, ... } : {
         publish.workstation = true;
         extraServiceFiles.ssh = "${pkgs.avahi}/etc/avahi/services/ssh.service";
       };
+    } // pkgs.lib.optionalAttrs (networkd) {
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+      };
     };
   in {
     one = cfg;
@@ -64,4 +76,4 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     two.succeed("avahi-browse -r -t _ssh._tcp | tee out >&2")
     two.succeed("test `wc -l < out` -gt 0")
   '';
-})
+} args
diff --git a/nixos/tests/awscli.nix b/nixos/tests/awscli.nix
new file mode 100644
index 0000000000000..e6741fcf14121
--- /dev/null
+++ b/nixos/tests/awscli.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "awscli";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.awscli ];
+    };
+
+  testScript =
+    ''
+      assert "${pkgs.python3Packages.botocore.version}" in machine.succeed("aws --version")
+      assert "${pkgs.awscli.version}" in machine.succeed("aws --version")
+    '';
+})
diff --git a/nixos/tests/babeld.nix b/nixos/tests/babeld.nix
index fafa788ba57bb..5817ea4ce142d 100644
--- a/nixos/tests/babeld.nix
+++ b/nixos/tests/babeld.nix
@@ -1,7 +1,7 @@
 
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "babeld";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hexa ];
   };
 
diff --git a/nixos/tests/bat.nix b/nixos/tests/bat.nix
new file mode 100644
index 0000000000000..0f548a590fb02
--- /dev/null
+++ b/nixos/tests/bat.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "bat";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  machine = { pkgs, ... }: { environment.systemPackages = [ pkgs.bat ]; };
+
+  testScript = ''
+    machine.succeed("echo 'Foobar\n\n\n42' > /tmp/foo")
+    assert "Foobar" in machine.succeed("bat -p /tmp/foo")
+    assert "42" in machine.succeed("bat -p /tmp/foo -r 4:4")
+  '';
+})
diff --git a/nixos/tests/bcachefs.nix b/nixos/tests/bcachefs.nix
index 3f116d7df92aa..146225e72ceed 100644
--- a/nixos/tests/bcachefs.nix
+++ b/nixos/tests/bcachefs.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "bcachefs";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ chiiruno ];
+  meta.maintainers = with pkgs.lib.maintainers; [ chiiruno ];
 
   machine = { pkgs, ... }: {
     virtualisation.emptyDiskImages = [ 4096 ];
diff --git a/nixos/tests/bees.nix b/nixos/tests/bees.nix
index 6e6a9c3446b06..58a9c29513567 100644
--- a/nixos/tests/bees.nix
+++ b/nixos/tests/bees.nix
@@ -8,7 +8,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
       ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux2 /dev/vdc
     '';
     virtualisation.emptyDiskImages = [ 4096 4096 ];
-    fileSystems = lib.mkVMOverride {
+    virtualisation.fileSystems = {
       "/aux1" = { # filesystem configured to be deduplicated
         device = "/dev/disk/by-label/aux1";
         fsType = "btrfs";
diff --git a/nixos/tests/bitcoind.nix b/nixos/tests/bitcoind.nix
index 09f3e4a6ec078..3e9e085287ac0 100644
--- a/nixos/tests/bitcoind.nix
+++ b/nixos/tests/bitcoind.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "bitcoind";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ _1000101 ];
   };
 
@@ -31,16 +31,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     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"\' '
+        'curl --fail --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"\' '
+        'curl --fail --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"\' '
+        'curl --fail --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"\' '
+        'curl --fail --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 c195b60cd5695..ee7a582922ce7 100644
--- a/nixos/tests/bittorrent.nix
+++ b/nixos/tests/bittorrent.nix
@@ -35,7 +35,7 @@ in
 
 {
   name = "bittorrent";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ domenkozar eelco rob bobvanderlinden ];
   };
 
diff --git a/nixos/tests/bitwarden.nix b/nixos/tests/bitwarden.nix
index a47c77cec2137..5345c7245d00d 100644
--- a/nixos/tests/bitwarden.nix
+++ b/nixos/tests/bitwarden.nix
@@ -27,7 +27,7 @@ let
   makeBitwardenTest = backend: makeTest {
     name = "bitwarden_rs-${backend}";
     meta = {
-      maintainers = with pkgs.stdenv.lib.maintainers; [ jjjollyjim ];
+      maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
     };
 
     nodes = {
diff --git a/nixos/tests/blockbook-frontend.nix b/nixos/tests/blockbook-frontend.nix
index 742a02999e745..e17a2d0577977 100644
--- a/nixos/tests/blockbook-frontend.nix
+++ b/nixos/tests/blockbook-frontend.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "blockbook-frontend";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ _1000101 ];
   };
 
diff --git a/nixos/tests/boot-stage1.nix b/nixos/tests/boot-stage1.nix
index cfb2ccb828563..ce86fc5f494df 100644
--- a/nixos/tests/boot-stage1.nix
+++ b/nixos/tests/boot-stage1.nix
@@ -158,5 +158,5 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     machine.succeed('pgrep -a -f "^kcanary$"')
   '';
 
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ aszlig ];
+  meta.maintainers = with pkgs.lib.maintainers; [ aszlig ];
 })
diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix
index bf37eb8607b28..fae1d2d071389 100644
--- a/nixos/tests/borgbackup.nix
+++ b/nixos/tests/borgbackup.nix
@@ -36,7 +36,7 @@ let
 
 in {
   name = "borgbackup";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ dotlambda ];
   };
 
diff --git a/nixos/tests/buildbot.nix b/nixos/tests/buildbot.nix
index 0d979dc2d0540..11f9fbef635ed 100644
--- a/nixos/tests/buildbot.nix
+++ b/nixos/tests/buildbot.nix
@@ -109,5 +109,5 @@ import ./make-test-python.nix {
         bbworker.fail("nc -z bbmaster 8011")
   '';
 
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ nand0p ];
+  meta.maintainers = with pkgs.lib.maintainers; [ nand0p ];
 } {}
diff --git a/nixos/tests/buildkite-agents.nix b/nixos/tests/buildkite-agents.nix
index a6f33e0143c5e..6674a0e884ed3 100644
--- a/nixos/tests/buildkite-agents.nix
+++ b/nixos/tests/buildkite-agents.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "buildkite-agent";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ flokli ];
   };
 
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index 445a7fa6b0b42..063f83a2f3d37 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "caddy";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ xfix filalex77 ];
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ xfix Br1ght0ne ];
   };
 
   nodes = {
@@ -57,11 +57,13 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
     def check_etag(url):
         etag = webserver.succeed(
-            "curl -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(url)
+            "curl --fail -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(
+                url
+            )
         )
         etag = etag.replace("\r\n", " ")
         http_code = webserver.succeed(
-            "curl --silent --show-error -o /dev/null -w \"%{{http_code}}\" --head -H 'If-None-Match: {}' {}".format(
+            "curl --fail --silent --show-error -o /dev/null -w \"%{{http_code}}\" --head -H 'If-None-Match: {}' {}".format(
                 etag, url
             )
         )
diff --git a/nixos/tests/cadvisor.nix b/nixos/tests/cadvisor.nix
index 60c04f1478003..c372dea301d29 100644
--- a/nixos/tests/cadvisor.nix
+++ b/nixos/tests/cadvisor.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... } : {
   name = "cadvisor";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ offline ];
   };
 
@@ -19,16 +19,16 @@ import ./make-test-python.nix ({ pkgs, ... } : {
   testScript =  ''
       start_all()
       machine.wait_for_unit("cadvisor.service")
-      machine.succeed("curl http://localhost:8080/containers/")
+      machine.succeed("curl -f http://localhost:8080/containers/")
 
       influxdb.wait_for_unit("influxdb.service")
 
       # create influxdb database
       influxdb.succeed(
-          'curl -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"'
+          'curl -f -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"'
       )
 
       influxdb.wait_for_unit("cadvisor.service")
-      influxdb.succeed("curl http://localhost:8080/containers/")
+      influxdb.succeed("curl -f http://localhost:8080/containers/")
     '';
 })
diff --git a/nixos/tests/cage.nix b/nixos/tests/cage.nix
index a6f73e00c066e..1ae07b6fd2ff9 100644
--- a/nixos/tests/cage.nix
+++ b/nixos/tests/cage.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
 {
   name = "cage";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ matthewbauer flokli ];
   };
 
diff --git a/nixos/tests/cagebreak.nix b/nixos/tests/cagebreak.nix
index ecbb69646a933..87f43cc3c3216 100644
--- a/nixos/tests/cagebreak.nix
+++ b/nixos/tests/cagebreak.nix
@@ -5,12 +5,11 @@ let
     workspaces 1
     escape C-t
     bind t exec env DISPLAY=:0 ${pkgs.xterm}/bin/xterm -cm -pc
-    bind a exec ${pkgs.alacritty}/bin/alacritty
   '';
 in
 {
   name = "cagebreak";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ berbiche ];
   };
 
@@ -20,7 +19,7 @@ in
   in {
     imports = [ ./common/user-account.nix ];
 
-    environment.systemPackages = [ pkgs.cagebreak ];
+    environment.systemPackages = [ pkgs.cagebreak pkgs.wallutils ];
     services.xserver = {
       enable = true;
       displayManager.autoLogin = {
@@ -32,7 +31,7 @@ in
       manage = "desktop";
       name = "cagebreak";
       start = ''
-        export XDG_RUNTIME_DIR=/run/user/${toString alice.uid}
+        export XDG_RUNTIME_DIR="/run/user/${toString alice.uid}"
         ${pkgs.cagebreak}/bin/cagebreak &
         waitPID=$!
       '';
@@ -74,24 +73,20 @@ in
 
   testScript = { nodes, ... }: let
     user = nodes.machine.config.users.users.alice;
+    XDG_RUNTIME_DIR = "/run/user/${toString user.uid}";
   in ''
     start_all()
     machine.wait_for_unit("multi-user.target")
-    machine.wait_for_file("/run/user/${toString user.uid}/wayland-0")
+    machine.wait_for_file("${XDG_RUNTIME_DIR}/wayland-0")
 
-    with subtest("ensure wayland works with alacritty"):
-        machine.send_key("ctrl-t")
-        machine.send_key("a")
-        machine.wait_until_succeeds("pgrep alacritty")
-        machine.wait_for_text("alice@machine")
-        machine.screenshot("screen")
-        machine.send_key("ctrl-d")
+    with subtest("ensure wayland works with wayinfo from wallutils"):
+        machine.succeed("env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} wayinfo")
 
     with subtest("ensure xwayland works with xterm"):
         machine.send_key("ctrl-t")
         machine.send_key("t")
         machine.wait_until_succeeds("pgrep xterm")
-        machine.wait_for_text("alice@machine")
+        machine.wait_for_text("${user.name}@machine")
         machine.screenshot("screen")
         machine.send_key("ctrl-d")
   '';
diff --git a/nixos/tests/cassandra.nix b/nixos/tests/cassandra.nix
index 05607956a9d66..bef3105f0a9eb 100644
--- a/nixos/tests/cassandra.nix
+++ b/nixos/tests/cassandra.nix
@@ -1,7 +1,5 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, testPackage ? pkgs.cassandra, ... }:
 let
-  # Change this to test a different version of Cassandra:
-  testPackage = pkgs.cassandra;
   clusterName = "NixOS Automated-Test Cluster";
 
   testRemoteAuth = lib.versionAtLeast testPackage.version "3.11";
@@ -47,7 +45,7 @@ let
   };
 in
 {
-  name = "cassandra";
+  name = "cassandra-${testPackage.version}";
   meta = {
     maintainers = with lib.maintainers; [ johnazoidberg ];
   };
@@ -128,4 +126,8 @@ in
             "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass2'"
         )
   '';
+
+  passthru = {
+    inherit testPackage;
+  };
 })
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index e26c6d5d670cf..4e6d644f96c8a 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -218,7 +218,7 @@ let
   '';
 in {
   name = "basic-multi-node-ceph-cluster";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lejonet ];
   };
 
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 98528f6317bc0..19919371a3ca7 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -184,7 +184,7 @@ let
   '';
 in {
   name = "basic-single-node-ceph-cluster";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lejonet johanot ];
   };
 
diff --git a/nixos/tests/certmgr.nix b/nixos/tests/certmgr.nix
index ef32f54400e30..8f5b89487793b 100644
--- a/nixos/tests/certmgr.nix
+++ b/nixos/tests/certmgr.nix
@@ -11,7 +11,7 @@ let
       file = {
         group = "nginx";
         owner = "nginx";
-        path = "/tmp/${host}-ca.pem";
+        path = "/var/ssl/${host}-ca.pem";
       };
       label = "www_ca";
       profile = "three-month";
@@ -20,13 +20,13 @@ let
     certificate = {
       group = "nginx";
       owner = "nginx";
-      path = "/tmp/${host}-cert.pem";
+      path = "/var/ssl/${host}-cert.pem";
     };
     private_key = {
       group = "nginx";
       mode = "0600";
       owner = "nginx";
-      path = "/tmp/${host}-key.pem";
+      path = "/var/ssl/${host}-key.pem";
     };
     request = {
       CN = host;
@@ -57,6 +57,8 @@ let
         services.cfssl.enable = true;
         systemd.services.cfssl.after = [ "cfssl-init.service" "networking.target" ];
 
+        systemd.tmpfiles.rules = [ "d /var/ssl 777 root root" ];
+
         systemd.services.cfssl-init = {
           description = "Initialize the cfssl CA";
           wantedBy    = [ "multi-user.target" ];
@@ -87,8 +89,8 @@ let
           enable = true;
           virtualHosts = lib.mkMerge (map (host: {
             ${host} = {
-              sslCertificate = "/tmp/${host}-cert.pem";
-              sslCertificateKey = "/tmp/${host}-key.pem";
+              sslCertificate = "/var/ssl/${host}-cert.pem";
+              sslCertificateKey = "/var/ssl/${host}-key.pem";
               extraConfig = ''
                 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
               '';
@@ -124,16 +126,18 @@ in
     };
     testScript = ''
       machine.wait_for_unit("cfssl.service")
-      machine.wait_until_succeeds("ls /tmp/decl.example.org-ca.pem")
-      machine.wait_until_succeeds("ls /tmp/decl.example.org-key.pem")
-      machine.wait_until_succeeds("ls /tmp/decl.example.org-cert.pem")
-      machine.wait_until_succeeds("ls /tmp/imp.example.org-ca.pem")
-      machine.wait_until_succeeds("ls /tmp/imp.example.org-key.pem")
-      machine.wait_until_succeeds("ls /tmp/imp.example.org-cert.pem")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-key.pem")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-cert.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-key.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-cert.pem")
       machine.wait_for_unit("nginx.service")
       assert 1 < int(machine.succeed('journalctl -u nginx | grep "Starting Nginx" | wc -l'))
-      machine.succeed("curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org")
-      machine.succeed("curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org")
+      machine.succeed("curl --cacert /var/ssl/imp.example.org-ca.pem https://imp.example.org")
+      machine.succeed(
+          "curl --cacert /var/ssl/decl.example.org-ca.pem https://decl.example.org"
+      )
     '';
   };
 
diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix
index e291fc285fba4..170f09d9b76cc 100644
--- a/nixos/tests/cfssl.nix
+++ b/nixos/tests/cfssl.nix
@@ -38,7 +38,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   testScript =
   let
     cfsslrequest = with pkgs; writeScript "cfsslrequest" ''
-      curl -X POST -H "Content-Type: application/json" -d @${csr} \
+      curl -f -X POST -H "Content-Type: application/json" -d @${csr} \
         http://localhost:8888/api/v1/cfssl/newkey | ${cfssl}/bin/cfssljson /tmp/certificate
     '';
     csr = pkgs.writeText "csr.json" (builtins.toJSON {
diff --git a/nixos/tests/charliecloud.nix b/nixos/tests/charliecloud.nix
index acba41e228a66..28c3e2f2dbf73 100644
--- a/nixos/tests/charliecloud.nix
+++ b/nixos/tests/charliecloud.nix
@@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, ...} : let
 
 in {
   name = "charliecloud";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bzizou ];
   };
 
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
index 795b93f6f54e5..8429d932ae694 100644
--- a/nixos/tests/chromium.nix
+++ b/nixos/tests/chromium.nix
@@ -1,10 +1,14 @@
 { system ? builtins.currentSystem
 , config ? {}
 , pkgs ? import ../.. { inherit system config; }
-, channelMap ? {
-    stable = pkgs.chromium;
-    beta   = pkgs.chromiumBeta;
-    dev    = pkgs.chromiumDev;
+, channelMap ? { # Maps "channels" to packages
+    stable        = pkgs.chromium;
+    beta          = pkgs.chromiumBeta;
+    dev           = pkgs.chromiumDev;
+    ungoogled     = pkgs.ungoogled-chromium;
+    chrome-stable = pkgs.google-chrome;
+    chrome-beta   = pkgs.google-chrome-beta;
+    chrome-dev    = pkgs.google-chrome-dev;
   }
 }:
 
@@ -14,7 +18,7 @@ with pkgs.lib;
 mapAttrs (channel: chromiumPkg: makeTest rec {
   name = "chromium-${channel}";
   meta = {
-    maintainers = with maintainers; [ aszlig ];
+    maintainers = with maintainers; [ aszlig primeos ];
     # https://github.com/NixOS/hydra/issues/591#issuecomment-435125621
     inherit (chromiumPkg.meta) timeout;
   };
@@ -47,7 +51,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
   testScript = let
     xdo = name: text: let
       xdoScript = pkgs.writeText "${name}.xdo" text;
-    in "${pkgs.xdotool}/bin/xdotool '${xdoScript}'";
+    in "${pkgs.xdotool}/bin/xdotool ${xdoScript}";
   in ''
     import shlex
     from contextlib import contextmanager, _GeneratorContextManager
@@ -58,101 +62,86 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         return "su - ${user} -c " + shlex.quote(cmd)
 
 
+    def get_browser_binary():
+        """Returns the name of the browser binary."""
+        pname = "${getName chromiumPkg.name}"
+        if pname.find("chromium") != -1:
+            return "chromium"  # Same name for all channels and ungoogled-chromium
+        if pname == "google-chrome":
+            return "google-chrome-stable"
+        if pname == "google-chrome-dev":
+            return "google-chrome-unstable"
+        # For google-chrome-beta and as fallback:
+        return pname
+
+
     def create_new_win():
+        """Creates a new Chromium window."""
         with machine.nested("Creating a new Chromium window"):
-            machine.execute(
+            machine.wait_until_succeeds(
                 ru(
-                    "${xdo "new-window" ''
+                    "${xdo "create_new_win-select_main_window" ''
                       search --onlyvisible --name "startup done"
                       windowfocus --sync
                       windowactivate --sync
                     ''}"
                 )
             )
-            machine.execute(
+            machine.send_key("ctrl-n")
+            # Wait until the new window appears:
+            machine.wait_until_succeeds(
                 ru(
-                    "${xdo "new-window" ''
-                      key Ctrl+n
-                    ''}"
-                )
-            )
-
-
-    def close_win():
-        def try_close(_):
-            machine.execute(
-                ru(
-                    "${xdo "close-window" ''
-                      search --onlyvisible --name "new tab"
+                    "${xdo "create_new_win-wait_for_window" ''
+                      search --onlyvisible --name "New Tab"
                       windowfocus --sync
                       windowactivate --sync
                     ''}"
                 )
             )
-            machine.execute(
-                ru(
-                    "${xdo "close-window" ''
-                      key Ctrl+w
-                    ''}"
-                )
-            )
-            for _ in range(1, 20):
-                status, out = machine.execute(
-                    ru(
-                        "${xdo "wait-for-close" ''
-                          search --onlyvisible --name "new tab"
-                        ''}"
-                    )
-                )
-                if status != 0:
-                    return True
-                machine.sleep(1)
-                return False
-
-        retry(try_close)
-
-
-    def wait_for_new_win():
-        ret = False
-        with machine.nested("Waiting for new Chromium window to appear"):
-            for _ in range(1, 20):
-                status, out = machine.execute(
-                    ru(
-                        "${xdo "wait-for-window" ''
-                          search --onlyvisible --name "new tab"
-                          windowfocus --sync
-                          windowactivate --sync
-                        ''}"
-                    )
-                )
-                if status == 0:
-                    ret = True
-                    machine.sleep(10)
-                    break
-                machine.sleep(1)
-        return ret
 
 
-    def create_and_wait_for_new_win():
-        for _ in range(1, 3):
-            create_new_win()
-            if wait_for_new_win():
-                return True
-        assert False, "new window did not appear within 60 seconds"
+    def close_new_tab_win():
+        """Closes the Chromium window with the title "New Tab"."""
+        machine.wait_until_succeeds(
+            ru(
+                "${xdo "close_new_tab_win-select_main_window" ''
+                  search --onlyvisible --name "New Tab"
+                  windowfocus --sync
+                  windowactivate --sync
+                ''}"
+            )
+        )
+        machine.send_key("ctrl-w")
+        # Wait until the closed window disappears:
+        machine.wait_until_fails(
+            ru(
+                "${xdo "close_new_tab_win-wait_for_close" ''
+                  search --onlyvisible --name "New Tab"
+                ''}"
+            )
+        )
 
 
     @contextmanager
     def test_new_win(description):
-        create_and_wait_for_new_win()
+        create_new_win()
         with machine.nested(description):
             yield
-        close_win()
+        # Close the newly created window:
+        machine.send_key("ctrl-w")
 
 
     machine.wait_for_x()
 
     url = "file://${startupHTML}"
-    machine.succeed(ru(f'ulimit -c unlimited; chromium "{url}" & disown'))
+    machine.succeed(ru(f'ulimit -c unlimited; "{get_browser_binary()}" "{url}" & disown'))
+
+    if get_browser_binary().startswith("google-chrome"):
+        # Need to click away the first window:
+        machine.wait_for_text("Make Google Chrome the default browser")
+        machine.screenshot("google_chrome_default_browser_prompt")
+        machine.send_key("ret")
+
     machine.wait_for_text("startup done")
     machine.wait_until_succeeds(
         ru(
@@ -166,9 +155,11 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         )
     )
 
-    create_and_wait_for_new_win()
+    create_new_win()
+    # Optional: Wait for the new tab page to fully load before taking the screenshot:
+    machine.wait_for_text("Web Store")
     machine.screenshot("empty_windows")
-    close_win()
+    close_new_tab_win()
 
     machine.screenshot("startup_done")
 
@@ -176,7 +167,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         machine.succeed(
             ru(
                 "${xdo "type-url" ''
-                  search --sync --onlyvisible --name "new tab"
+                  search --sync --onlyvisible --name "New Tab"
                   windowfocus --sync
                   type --delay 1000 "chrome://sandbox"
                 ''}"
@@ -186,7 +177,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         machine.succeed(
             ru(
                 "${xdo "submit-url" ''
-                  search --sync --onlyvisible --name "new tab"
+                  search --sync --onlyvisible --name "New Tab"
                   windowfocus --sync
                   key --delay 1000 Return
                 ''}"
@@ -198,7 +189,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         machine.succeed(
             ru(
                 "${xdo "find-window" ''
-                  search --sync --onlyvisible --name "sandbox status"
+                  search --sync --onlyvisible --name "Sandbox Status"
                   windowfocus --sync
                 ''}"
             )
@@ -232,7 +223,7 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
         machine.succeed(
             ru(
                 "${xdo "find-window-after-copy" ''
-                  search --onlyvisible --name "sandbox status"
+                  search --onlyvisible --name "Sandbox Status"
                 ''}"
             )
         )
diff --git a/nixos/tests/cifs-utils.nix b/nixos/tests/cifs-utils.nix
new file mode 100644
index 0000000000000..98587b10d941a
--- /dev/null
+++ b/nixos/tests/cifs-utils.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "cifs-utils";
+
+  machine = { pkgs, ... }: { environment.systemPackages = [ pkgs.cifs-utils ]; };
+
+  testScript = ''
+    machine.succeed("smbinfo -h")
+    machine.succeed("smb2-quota -h")
+    assert "${pkgs.cifs-utils.version}" in machine.succeed("cifs.upcall -v")
+    assert "${pkgs.cifs-utils.version}" in machine.succeed("mount.cifs -V")
+  '';
+})
diff --git a/nixos/tests/cjdns.nix b/nixos/tests/cjdns.nix
index d72236d415d4d..dc5f371c74d83 100644
--- a/nixos/tests/cjdns.nix
+++ b/nixos/tests/cjdns.nix
@@ -19,7 +19,7 @@ in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cjdns";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ehmry ];
   };
 
diff --git a/nixos/tests/clickhouse.nix b/nixos/tests/clickhouse.nix
index 2d8a7cf7aa9fa..98d8b4b465257 100644
--- a/nixos/tests/clickhouse.nix
+++ b/nixos/tests/clickhouse.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "clickhouse";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ ma27 ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
 
   machine = {
     services.clickhouse.enable = true;
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index aafa6e24e84a3..e06cbd056a323 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -7,6 +7,9 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
+  inherit (import ./ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+
   metadataDrive = pkgs.stdenv.mkDerivation {
     name = "metadata";
     buildCommand = ''
@@ -18,35 +21,64 @@ let
       -   content: |
                 cloudinit
           path: /tmp/cloudinit-write-file
+
+      users:
+        - default
+        - name: nixos
+          ssh_authorized_keys:
+            - "${snakeOilPublicKey}"
       EOF
 
       cat << EOF > $out/iso/meta-data
       instance-id: iid-local01
       local-hostname: "test"
       public-keys:
-          - "should be a key!"
+        - "${snakeOilPublicKey}"
       EOF
       ${pkgs.cdrkit}/bin/genisoimage -volid cidata -joliet -rock -o $out/metadata.iso $out/iso
       '';
   };
 in makeTest {
   name = "cloud-init";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lewo ];
   };
-  machine =
-    { ... }:
-    {
-      virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
-      services.cloud-init.enable = true;
-    };
+  machine = { ... }:
+  {
+    virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
+    services.cloud-init.enable = true;
+    services.openssh.enable = true;
+    networking.hostName = "";
+  };
   testScript = ''
-      machine.start()
-      machine.wait_for_unit("cloud-init.service")
-      machine.succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'")
+    # To wait until cloud-init terminates its run
+    unnamed.wait_for_unit("cloud-final.service")
+
+    unnamed.succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'")
+
+    # install snakeoil ssh key and provision .ssh/config file
+    unnamed.succeed("mkdir -p ~/.ssh")
+    unnamed.succeed(
+        "cat ${snakeOilPrivateKey} > ~/.ssh/id_snakeoil"
+    )
+    unnamed.succeed("chmod 600 ~/.ssh/id_snakeoil")
+
+    unnamed.wait_for_unit("sshd.service")
+
+    # we should be able to log in as the root user, as well as the created nixos user
+    unnamed.succeed(
+        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil root@localhost 'true'"
+    )
+    unnamed.succeed(
+        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'true'"
+    )
 
-      machine.wait_until_succeeds(
-          "cat /root/.ssh/authorized_keys | grep -q 'should be a key!'"
-      )
+    # test changing hostname via cloud-init worked
+    assert (
+        unnamed.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'hostname'"
+        ).strip()
+        == "test"
+    )
   '';
 }
diff --git a/nixos/tests/cockroachdb.nix b/nixos/tests/cockroachdb.nix
index d0cc5e19837c6..d793842f0ab2d 100644
--- a/nixos/tests/cockroachdb.nix
+++ b/nixos/tests/cockroachdb.nix
@@ -99,7 +99,7 @@ let
 
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cockroachdb";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers;
+  meta.maintainers = with pkgs.lib.maintainers;
     [ thoughtpolice ];
 
   nodes = {
diff --git a/nixos/tests/codimd.nix b/nixos/tests/codimd.nix
deleted file mode 100644
index aa581dfeb584b..0000000000000
--- a/nixos/tests/codimd.nix
+++ /dev/null
@@ -1,60 +0,0 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }:
-{
-  name = "codimd";
-
-  meta = with lib.maintainers; {
-    maintainers = [ willibutz ];
-  };
-
-  nodes = {
-    codimdSqlite = { ... }: {
-      services = {
-        codimd = {
-          enable = true;
-          configuration.dbURL = "sqlite:///var/lib/codimd/codimd.db";
-        };
-      };
-    };
-
-    codimdPostgres = { ... }: {
-      systemd.services.codimd.after = [ "postgresql.service" ];
-      services = {
-        codimd = {
-          enable = true;
-          configuration.dbURL = "postgres://codimd:\${DB_PASSWORD}@localhost:5432/codimddb";
-
-          /*
-           * Do not use pkgs.writeText for secrets as
-           * they will end up in the world-readable Nix store.
-           */
-          environmentFile = pkgs.writeText "codimd-env" ''
-            DB_PASSWORD=snakeoilpassword
-          '';
-        };
-        postgresql = {
-          enable = true;
-          initialScript = pkgs.writeText "pg-init-script.sql" ''
-            CREATE ROLE codimd LOGIN PASSWORD 'snakeoilpassword';
-            CREATE DATABASE codimddb OWNER codimd;
-          '';
-        };
-      };
-    };
-  };
-
-  testScript = ''
-    start_all()
-
-    with subtest("CodiMD sqlite"):
-        codimdSqlite.wait_for_unit("codimd.service")
-        codimdSqlite.wait_for_open_port(3000)
-        codimdSqlite.wait_until_succeeds("curl -sSf http://localhost:3000/new")
-
-    with subtest("CodiMD postgres"):
-        codimdPostgres.wait_for_unit("postgresql.service")
-        codimdPostgres.wait_for_unit("codimd.service")
-        codimdPostgres.wait_for_open_port(5432)
-        codimdPostgres.wait_for_open_port(3000)
-        codimdPostgres.wait_until_succeeds("curl -sSf http://localhost:3000/new")
-  '';
-})
diff --git a/nixos/tests/common/acme/server/README.md b/nixos/tests/common/acme/server/README.md
new file mode 100644
index 0000000000000..9de2b2c710292
--- /dev/null
+++ b/nixos/tests/common/acme/server/README.md
@@ -0,0 +1,21 @@
+# Fake Certificate Authority for ACME testing
+
+This will set up a test node running [pebble](https://github.com/letsencrypt/pebble)
+to serve ACME certificate requests.
+
+## "Snake oil" certs
+
+The snake oil certs are hard coded into the repo for reasons explained [here](https://github.com/NixOS/nixpkgs/pull/91121#discussion_r505410235).
+The root of the issue is that Nix will hash the derivation based on the arguments
+to mkDerivation, not the output. [Minica](https://github.com/jsha/minica) will
+always generate a random certificate even if the arguments are unchanged. As a
+result, it's possible to end up in a situation where the cached and local
+generated certs mismatch and cause issues with testing.
+
+To generate new certificates, run the following commands:
+
+```bash
+nix-build generate-certs.nix
+cp result/* .
+rm result
+```
diff --git a/nixos/tests/common/acme/server/acme.test.cert.pem b/nixos/tests/common/acme/server/acme.test.cert.pem
new file mode 100644
index 0000000000000..76b0d916a8175
--- /dev/null
+++ b/nixos/tests/common/acme/server/acme.test.cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDLDCCAhSgAwIBAgIIRDAN3FHH//IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgNzg3NDZmMB4XDTIwMTAyMTEzMjgzNloXDTIyMTEy
+MDEzMjgzNlowFDESMBAGA1UEAxMJYWNtZS50ZXN0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAo8XjMVUaljcaqQ5MFhfPuQgSwdyXEUbpSHz+5yPkE0h9
+Z4Xu5BJF1Oq7h5ggCtadVsIspiY6Jm6aWDOjlh4myzW5UNBNUG3OPEk50vmmHFeH
+pImHO/d8yb33QoF9VRcTZs4tuJYg7l9bSs4jNG72vYvv2YiGAcmjJcsmAZIfniCN
+Xf/LjIm+Cxykn+Vo3UuzO1w5/iuofdgWO/aZxMezmXUivlL3ih4cNzCJei8WlB/l
+EnHrkcy3ogRmmynP5zcz7vmGIJX2ji6dhCa4Got5B7eZK76o2QglhQXqPatG0AOY
+H+RfQfzKemqPG5om9MgJtwFtTOU1LoaiBw//jXKESQIDAQABo3YwdDAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB
+/wQCMAAwHwYDVR0jBBgwFoAU+8IZlLV/Qp5CXqpXMLvtxWlxcJwwFAYDVR0RBA0w
+C4IJYWNtZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBAQB0pe8I5/VDkB5VMgQB2GJV
+GKzyigfWbVez9uLmqMj9PPP/zzYKSYeq+91aMuOZrnH7NqBxSTwanULkmqAmhbJJ
+YkXw+FlFekf9FyxcuArzwzzNZDSGcjcdXpN8S2K1qkBd00iSJF9kU7pdZYCIKR20
+QirdBrELEfsJ3GU62a6N3a2YsrisZUvq5TbjGJDcytAtt+WG3gmV7RInLdFfPwbw
+bEHPCnx0uiV0nxLjd/aVT+RceVrFQVt4hR99jLoMlBitSKluZ1ljsrpIyroBhQT0
+pp/pVi6HJdijG0fsPrC325NEGAwcpotLUhczoeM/rffKJd54wLhDkfYxOyRZXivs
+-----END CERTIFICATE-----
diff --git a/nixos/tests/common/acme/server/acme.test.key.pem b/nixos/tests/common/acme/server/acme.test.key.pem
new file mode 100644
index 0000000000000..741df99a372e3
--- /dev/null
+++ b/nixos/tests/common/acme/server/acme.test.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAo8XjMVUaljcaqQ5MFhfPuQgSwdyXEUbpSHz+5yPkE0h9Z4Xu
+5BJF1Oq7h5ggCtadVsIspiY6Jm6aWDOjlh4myzW5UNBNUG3OPEk50vmmHFeHpImH
+O/d8yb33QoF9VRcTZs4tuJYg7l9bSs4jNG72vYvv2YiGAcmjJcsmAZIfniCNXf/L
+jIm+Cxykn+Vo3UuzO1w5/iuofdgWO/aZxMezmXUivlL3ih4cNzCJei8WlB/lEnHr
+kcy3ogRmmynP5zcz7vmGIJX2ji6dhCa4Got5B7eZK76o2QglhQXqPatG0AOYH+Rf
+QfzKemqPG5om9MgJtwFtTOU1LoaiBw//jXKESQIDAQABAoIBADox/2FwVFo8ioS4
+R+Ex5OZjMAcjU6sX/516jTmlT05q2+UFerYgqB/YqXqtW/V9/brulN8VhmRRuRbO
+grq9TBu5o3hMDK0f18EkZB/MBnLbx594H033y6gEkPBZAyhRYtuNOEH3VwxdZhtW
+1Lu1EoiYSUqLcNMBy6+KWJ8GRaXyacMYBlj2lMHmyzkA/t1+2mwTGC3lT6zN0F5Y
+E5umXOxsn6Tb6q3KM9O5IvtmMMKpgj4HIHZLZ6j40nNgHwGRaAv4Sha/vx0DeBw3
+6VlNiTTPdShEkhESlM5/ocqTfI92VHJpM5gkqTYOWBi2aKIPfAopXoqoJdWl4pQ/
+NCFIu2ECgYEAzntNKIcQtf0ewe0/POo07SIFirvz6jVtYNMTzeQfL6CoEjYArJeu
+Vzc4wEQfA4ZFVerBb1/O6M449gI3zex1PH4AX0h8q8DSjrppK1Jt2TnpVh97k7Gg
+Tnat/M/yW3lWYkcMVJJ3AYurXLFTT1dYP0HvBwZN04yInrEcPNXKfmcCgYEAywyJ
+51d4AE94PrANathKqSI/gk8sP+L1gzylZCcUEAiGk/1r45iYB4HN2gvWbS+CvSdp
+F7ShlDWrTaNh2Bm1dgTjc4pWb4J+CPy/KN2sgLwIuM4+ZWIZmEDcio6khrM/gNqK
+aR7xUsvWsqU26O84woY/xR8IHjSNF7cFWE1H2c8CgYEAt6SSi2kVQ8dMg84uYE8t
+o3qO00U3OycpkOQqyQQLeKC62veMwfRl6swCfX4Y11mkcTXJtPTRYd2Ia8StPUkB
+PDwUuKoPt/JXUvoYb59wc7M+BIsbrdBdc2u6cw+/zfutCNuH6/AYSBeg4WAVaIuW
+wSwzG1xP+8cR+5IqOzEqWCECgYATweeVTCyQEyuHJghYMi2poXx+iIesu7/aAkex
+pB/Oo5W8xrb90XZRnK7UHbzCqRHWqAQQ23Gxgztk9ZXqui2vCzC6qGZauV7cLwPG
+zTMg36sVmHP314DYEM+k59ZYiQ6P0jQPoIQo407D2VGrfsOOIhQIcUmP7tsfyJ5L
+hlGMfwKBgGq4VNnnuX8I5kl03NpaKfG+M8jEHmVwtI9RkPTCCX9bMjeG0cDxqPTF
+TRkf3r8UWQTZ5QfAfAXYAOlZvmGhHjSembRbXMrMdi3rGsYRSrQL6n5NHnORUaMy
+FCWo4gyAnniry7tx9dVNgmHmbjEHuQnf8AC1r3dibRCjvJWUiQ8H
+-----END RSA PRIVATE KEY-----
diff --git a/nixos/tests/common/acme/server/ca.cert.pem b/nixos/tests/common/acme/server/ca.cert.pem
new file mode 100644
index 0000000000000..5c33e879b675b
--- /dev/null
+++ b/nixos/tests/common/acme/server/ca.cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSzCCAjOgAwIBAgIIeHRvRrNvbGQwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgNzg3NDZmMCAXDTIwMTAyMTEzMjgzNloYDzIxMjAx
+MDIxMTMyODM2WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA3ODc0NmYwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrNTzVLDJOKtGYGLU98EEcLKps
+tXHCLC6G54LKbEcU80fn+ArX8qsPSHyhdXQkcYjq6Vh/EDJ1TctyRSnvAjwyG4Aa
+1Zy1QFc/JnjMjvzimCkUc9lQ+wkLwHSM/KGwR1cGjmtQ/EMClZTA0NwulJsXMKVz
+bd5asXbq/yJTQ5Ww25HtdNjwRQXTvB7r3IKcY+DsED9CvFvC9oG/ZhtZqZuyyRdC
+kFUrrv8WNUDkWSN+lMR6xMx8v0583IN6f11IhX0b+svK98G81B2eswBdkzvVyv9M
+unZBO0JuJG8sdM502KhWLmzBC1ZbvgUBF9BumDRpMFH4DCj7+qQ2taWeGyc7AgMB
+AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
+BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBT7whmUtX9CnkJe
+qlcwu+3FaXFwnDAfBgNVHSMEGDAWgBT7whmUtX9CnkJeqlcwu+3FaXFwnDANBgkq
+hkiG9w0BAQsFAAOCAQEARMe1wKmF33GjEoLLw0oDDS4EdAv26BzCwtrlljsEtwQN
+95oSzUNd6o4Js7WCG2o543OX6cxzM+yju8TES3+vJKDgsbNMU0bWCv//tdrb0/G8
+OkU3Kfi5q4fOauZ1pqGv/pXdfYhZ5ieB/zwis3ykANe5JfB0XqwCb1Vd0C3UCIS2
+NPKngRwNSzphIsbzfvxGDkdM1enuGl5CVyDhrwTMqGaJGDSOv6U5jKFxKRvigqTN
+Ls9lPmT5NXYETduWLBR3yUIdH6kZXrcozZ02B9vjOB2Cv4RMDc+9eM30CLIWpf1I
+097e7JkhzxFhfC/bMMt3P1FeQc+fwH91wdBmNi7tQw==
+-----END CERTIFICATE-----
diff --git a/nixos/tests/common/acme/server/ca.key.pem b/nixos/tests/common/acme/server/ca.key.pem
new file mode 100644
index 0000000000000..ed46f5dccf467
--- /dev/null
+++ b/nixos/tests/common/acme/server/ca.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAqzU81SwyTirRmBi1PfBBHCyqbLVxwiwuhueCymxHFPNH5/gK
+1/KrD0h8oXV0JHGI6ulYfxAydU3LckUp7wI8MhuAGtWctUBXPyZ4zI784pgpFHPZ
+UPsJC8B0jPyhsEdXBo5rUPxDApWUwNDcLpSbFzClc23eWrF26v8iU0OVsNuR7XTY
+8EUF07we69yCnGPg7BA/QrxbwvaBv2YbWambsskXQpBVK67/FjVA5FkjfpTEesTM
+fL9OfNyDen9dSIV9G/rLyvfBvNQdnrMAXZM71cr/TLp2QTtCbiRvLHTOdNioVi5s
+wQtWW74FARfQbpg0aTBR+Awo+/qkNrWlnhsnOwIDAQABAoIBAA3ykVkgd5ysmlSU
+trcsCnHcJaojgff6l3PACoSpG4VWaGY6a8+54julgRm6MtMBONFCX0ZCsImj484U
+Wl0xRmwil2YYPuL5MeJgJPktMObY1IfpBCw3tz3w2M3fiuCMf0d2dMGtO1xLiUnH
++hgFXTkfamsj6ThkOrbcQBSebeRxbKM5hqyCaQoieV+0IJnyxUVq/apib8N50VsH
+SHd4oqLUuEZgg6N70+l5DpzedJUb4nrwS/KhUHUBgnoPItYBCiGPmrwLk7fUhPs6
+kTDqJDtc/xW/JbjmzhWEpVvtumcC/OEKULss7HLdeQqwVBrRQkznb0M9AnSra3d0
+X11/Y4ECgYEA3FC8SquLPFb2lHK4+YbJ4Ac6QVWeYFEHiZ0Rj+CmONmjcAvOGLPE
+SblRLm3Nbrkxbm8FF6/AfXa/rviAKEVPs5xqGfSDw/3n1uInPcmShiBCLwM/jHH5
+NeVG+R5mTg5zyQ/pQMLWRcs+Ail+ZAnZuoGpW3Cdc8OtCUYFQ7XB6nsCgYEAxvBJ
+zFxcTtsDzWbMWXejugQiUqJcEbKWwEfkRbf3J2rAVO2+EFr7LxdRfN2VwPiTQcWc
+LnN2QN+ouOjqBMTh3qm5oQY+TLLHy86k9g1k0gXWkMRQgP2ZdfWH1HyrwjLUgLe1
+VezFN7N1azgy6xFkInAAvuA4loxElZNvkGBgekECgYA/Xw26ILvNIGqO6qzgQXAh
++5I7JsiGheg4IjDiBMlrQtbrLMoceuD0H9UFGNplhel9DXwWgxxIOncKejpK2x0A
+2fX+/0FDh+4+9hA5ipiV8gN3iGSoHkSDxy5yC9d7jlapt+TtFt4Rd1OfxZWwatDw
+/8jaH3t6yAcmyrhK8KYVrwKBgAE5KwsBqmOlvyE9N5Z5QN189wUREIXfVkP6bTHs
+jq2EX4hmKdwJ4y+H8i1VY31bSfSGlY5HkXuWpH/2lrHO0CDBZG3UDwADvWzIaYVF
+0c/kz0v2mRQh+xaZmus4lQnNrDbaalgL666LAPbW0qFVaws3KxoBYPe0BxvwWyhF
+H3LBAoGBAKRRNsq2pWQ8Gqxc0rVoH0FlexU9U2ci3lsLmgEB0A/o/kQkSyAxaRM+
+VdKp3sWfO8o8lX5CVQslCNBSjDTNcat3Co4NEBLg6Xv1yKN/WN1GhusnchP9szsP
+oU47gC89QhUyWSd6vvr2z2NG9C3cACxe4dhDSHQcE4nHSldzCKv2
+-----END RSA PRIVATE KEY-----
diff --git a/nixos/tests/common/acme/server/default.nix b/nixos/tests/common/acme/server/default.nix
index 4d8e664c4e17f..1c3bfdf76b7e7 100644
--- a/nixos/tests/common/acme/server/default.nix
+++ b/nixos/tests/common/acme/server/default.nix
@@ -51,10 +51,7 @@
 # that it has to be started _before_ the ACME service.
 { config, pkgs, lib, ... }:
 let
-  testCerts = import ./snakeoil-certs.nix {
-    minica = pkgs.minica;
-    mkDerivation = pkgs.stdenv.mkDerivation;
-  };
+  testCerts = import ./snakeoil-certs.nix;
   domain = testCerts.domain;
 
   resolver = let
@@ -70,7 +67,7 @@ let
     privateKey = testCerts.${domain}.key;
     httpPort = 80;
     tlsPort = 443;
-    ocspResponderURL = "http://0.0.0.0:4002";
+    ocspResponderURL = "http://${domain}:4002";
     strict = true;
   };
 
diff --git a/nixos/tests/common/acme/server/generate-certs.nix b/nixos/tests/common/acme/server/generate-certs.nix
new file mode 100644
index 0000000000000..cd8fe0dffca17
--- /dev/null
+++ b/nixos/tests/common/acme/server/generate-certs.nix
@@ -0,0 +1,29 @@
+# Minica can provide a CA key and cert, plus a key
+# and cert for our fake CA server's Web Front End (WFE).
+{
+  pkgs ? import <nixpkgs> {},
+  minica ? pkgs.minica,
+  mkDerivation ? pkgs.stdenv.mkDerivation
+}:
+let
+  conf = import ./snakeoil-certs.nix;
+  domain = conf.domain;
+in mkDerivation {
+  name = "test-certs";
+  buildInputs = [ minica ];
+  phases = [ "buildPhase" "installPhase" ];
+
+  buildPhase = ''
+    minica \
+      --ca-key ca.key.pem \
+      --ca-cert ca.cert.pem \
+      --domains ${domain}
+  '';
+
+  installPhase = ''
+    mkdir -p $out
+    mv ca.*.pem $out/
+    mv ${domain}/key.pem $out/${domain}.key.pem
+    mv ${domain}/cert.pem $out/${domain}.cert.pem
+  '';
+}
diff --git a/nixos/tests/common/acme/server/snakeoil-certs.nix b/nixos/tests/common/acme/server/snakeoil-certs.nix
index 4b6a38b8fa307..11c3f7fc9290c 100644
--- a/nixos/tests/common/acme/server/snakeoil-certs.nix
+++ b/nixos/tests/common/acme/server/snakeoil-certs.nix
@@ -1,37 +1,13 @@
-# Minica can provide a CA key and cert, plus a key
-# and cert for our fake CA server's Web Front End (WFE).
-{ minica, mkDerivation }:
 let
   domain = "acme.test";
-
-  selfSignedCertData = mkDerivation {
-    name = "test-certs";
-    buildInputs = [ minica ];
-    phases = [ "buildPhase" "installPhase" ];
-
-    buildPhase = ''
-      mkdir ca
-      minica \
-        --ca-key ca/key.pem \
-        --ca-cert ca/cert.pem \
-        --domains ${domain}
-      chmod 600 ca/*
-      chmod 640 ${domain}/*.pem
-    '';
-
-    installPhase = ''
-      mkdir -p $out
-      mv ${domain} ca $out/
-    '';
-  };
 in {
   inherit domain;
   ca = {
-    cert = "${selfSignedCertData}/ca/cert.pem";
-    key = "${selfSignedCertData}/ca/key.pem";
+    cert = ./ca.cert.pem;
+    key = ./ca.key.pem;
   };
   "${domain}" = {
-    cert = "${selfSignedCertData}/${domain}/cert.pem";
-    key = "${selfSignedCertData}/${domain}/key.pem";
+    cert = ./. + "/${domain}.cert.pem";
+    key = ./. + "/${domain}.key.pem";
   };
 }
diff --git a/nixos/tests/containers-bridge.nix b/nixos/tests/containers-bridge.nix
index 2c8e8fa5370f0..1208aa8fced7e 100644
--- a/nixos/tests/containers-bridge.nix
+++ b/nixos/tests/containers-bridge.nix
@@ -9,7 +9,7 @@ in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-bridge";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-custom-pkgs.nix b/nixos/tests/containers-custom-pkgs.nix
index 397a4a905e6d9..1412c32bfb5f1 100644
--- a/nixos/tests/containers-custom-pkgs.nix
+++ b/nixos/tests/containers-custom-pkgs.nix
@@ -1,42 +1,34 @@
-# Test for NixOS' container support.
-
 import ./make-test-python.nix ({ pkgs, lib, ...} : let
 
-  customPkgs = pkgs // {
-    hello = pkgs.hello.overrideAttrs(old: {
-      name = "custom-hello";
+  customPkgs = pkgs.appendOverlays [ (self: super: {
+    hello = super.hello.overrideAttrs (old: {
+       name = "custom-hello";
     });
-  };
+  }) ];
 
 in {
-  name = "containers-hosts";
+  name = "containers-custom-pkgs";
   meta = with lib.maintainers; {
-    maintainers = [ adisbladis ];
+    maintainers = [ adisbladis earvstedt ];
   };
 
-  machine =
-    { ... }:
-    {
-      virtualisation.memorySize = 256;
-      virtualisation.vlans = [];
+  machine = { config, ... }: {
+    assertions = let
+      helloName = (builtins.head config.containers.test.config.system.extraDependencies).name;
+    in [ {
+      assertion = helloName == "custom-hello";
+      message = "Unexpected value: ${helloName}";
+    } ];
 
-      containers.simple = {
-        autoStart = true;
-        pkgs = customPkgs;
-        config = {pkgs, config, ... }: {
-          environment.systemPackages = [
-            pkgs.hello
-          ];
-        };
+    containers.test = {
+      autoStart = true;
+      config = { pkgs, config, ... }: {
+        nixpkgs.pkgs = customPkgs;
+        system.extraDependencies = [ pkgs.hello ];
       };
-
     };
+  };
 
-  testScript = ''
-    start_all()
-    machine.wait_for_unit("default.target")
-    machine.succeed(
-        "test $(nixos-container run simple -- readlink -f /run/current-system/sw/bin/hello) = ${customPkgs.hello}/bin/hello"
-    )
-  '';
+  # This test only consists of evaluating the test machine
+  testScript = "";
 })
diff --git a/nixos/tests/containers-extra_veth.nix b/nixos/tests/containers-extra_veth.nix
index 7d30b3f76cd7b..212f3d0f46cbc 100644
--- a/nixos/tests/containers-extra_veth.nix
+++ b/nixos/tests/containers-extra_veth.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-extra_veth";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-hosts.nix b/nixos/tests/containers-hosts.nix
index d6fb4a761eef7..65a983c42a789 100644
--- a/nixos/tests/containers-hosts.nix
+++ b/nixos/tests/containers-hosts.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-hosts";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ montag451 ];
   };
 
diff --git a/nixos/tests/containers-imperative.nix b/nixos/tests/containers-imperative.nix
index c4f2002918fc7..393b4a5135dd5 100644
--- a/nixos/tests/containers-imperative.nix
+++ b/nixos/tests/containers-imperative.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-imperative";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-ip.nix b/nixos/tests/containers-ip.nix
index 8583a08c62589..0265ed92d41cb 100644
--- a/nixos/tests/containers-ip.nix
+++ b/nixos/tests/containers-ip.nix
@@ -15,7 +15,7 @@ let
 
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-ipv4-ipv6";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-macvlans.nix b/nixos/tests/containers-macvlans.nix
index 0e8f67bc76f0b..9425252cb8862 100644
--- a/nixos/tests/containers-macvlans.nix
+++ b/nixos/tests/containers-macvlans.nix
@@ -8,7 +8,7 @@ in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-macvlans";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ montag451 ];
   };
 
diff --git a/nixos/tests/containers-physical_interfaces.nix b/nixos/tests/containers-physical_interfaces.nix
index e800751a23c23..0b55c3418edfb 100644
--- a/nixos/tests/containers-physical_interfaces.nix
+++ b/nixos/tests/containers-physical_interfaces.nix
@@ -1,7 +1,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-physical_interfaces";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-portforward.nix b/nixos/tests/containers-portforward.nix
index 1e2c2c6c374f5..d0be3c7d43ec6 100644
--- a/nixos/tests/containers-portforward.nix
+++ b/nixos/tests/containers-portforward.nix
@@ -9,7 +9,7 @@ in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-portforward";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aristid aszlig eelco kampfschlaefer ianwookim ];
   };
 
diff --git a/nixos/tests/containers-reloadable.nix b/nixos/tests/containers-reloadable.nix
index 2d81f1639387d..8772469176727 100644
--- a/nixos/tests/containers-reloadable.nix
+++ b/nixos/tests/containers-reloadable.nix
@@ -16,7 +16,7 @@ let
   };
 in {
   name = "containers-reloadable";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ danbst ];
   };
 
diff --git a/nixos/tests/containers-restart_networking.nix b/nixos/tests/containers-restart_networking.nix
index b50dadd13e479..b35552b5b1919 100644
--- a/nixos/tests/containers-restart_networking.nix
+++ b/nixos/tests/containers-restart_networking.nix
@@ -19,7 +19,7 @@ let
 in import ./make-test-python.nix ({ pkgs, ...} :
 {
   name = "containers-restart_networking";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ kampfschlaefer ];
   };
 
diff --git a/nixos/tests/containers-tmpfs.nix b/nixos/tests/containers-tmpfs.nix
index 171e8f01c7b95..7ebf0d02a2406 100644
--- a/nixos/tests/containers-tmpfs.nix
+++ b/nixos/tests/containers-tmpfs.nix
@@ -2,8 +2,8 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "containers-tmpfs";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ kampka ];
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
   };
 
   machine =
diff --git a/nixos/tests/convos.nix b/nixos/tests/convos.nix
index b4ff1188fd8be..a13870d170843 100644
--- a/nixos/tests/convos.nix
+++ b/nixos/tests/convos.nix
@@ -6,7 +6,7 @@ let
 in
 {
   name = "convos";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ sgo ];
   };
 
@@ -25,6 +25,6 @@ in
     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}/")
+    machine.succeed("curl -f http://localhost:${toString port}/")
   '';
 })
diff --git a/nixos/tests/corerad.nix b/nixos/tests/corerad.nix
index 37a1e90477a82..638010f92f445 100644
--- a/nixos/tests/corerad.nix
+++ b/nixos/tests/corerad.nix
@@ -80,7 +80,7 @@ import ./make-test-python.nix (
           ), "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")
+          out = router.succeed("curl -f localhost:9430/metrics")
 
           assert (
               "corerad_build_info" in out
diff --git a/nixos/tests/couchdb.nix b/nixos/tests/couchdb.nix
index 57b79e29b4330..d038ee7d890dc 100644
--- a/nixos/tests/couchdb.nix
+++ b/nixos/tests/couchdb.nix
@@ -19,7 +19,7 @@ with lib;
 
 {
   name = "couchdb";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ fpletz ];
   };
 
diff --git a/nixos/tests/cri-o.nix b/nixos/tests/cri-o.nix
index f13f1bdacb6af..91d46657f2411 100644
--- a/nixos/tests/cri-o.nix
+++ b/nixos/tests/cri-o.nix
@@ -1,7 +1,7 @@
 # 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;
+  maintainers = with pkgs.lib.maintainers; teams.podman.members;
 
   nodes = {
     crio = {
diff --git a/nixos/tests/custom-ca.nix b/nixos/tests/custom-ca.nix
new file mode 100644
index 0000000000000..67f7b3ff1f161
--- /dev/null
+++ b/nixos/tests/custom-ca.nix
@@ -0,0 +1,161 @@
+# Checks that `security.pki` options are working in curl and the main browser
+# engines: Gecko (via Firefox), Chromium, QtWebEngine (Falkon) and WebKitGTK
+# (via Midori). The test checks that certificates issued by a custom trusted
+# CA are accepted but those from an unknown CA are rejected.
+
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  makeCert = { caName, domain }: pkgs.runCommand "example-cert"
+  { buildInputs = [ pkgs.gnutls ]; }
+  ''
+    mkdir $out
+
+    # CA cert template
+    cat >ca.template <<EOF
+    organization = "${caName}"
+    cn = "${caName}"
+    expiration_days = 365
+    ca
+    cert_signing_key
+    crl_signing_key
+    EOF
+
+    # server cert template
+    cat >server.template <<EOF
+    organization = "An example company"
+    cn = "${domain}"
+    expiration_days = 30
+    dns_name = "${domain}"
+    encryption_key
+    signing_key
+    EOF
+
+    # generate CA keypair
+    certtool                \
+      --generate-privkey    \
+      --key-type rsa        \
+      --sec-param High      \
+      --outfile $out/ca.key
+    certtool                     \
+      --generate-self-signed     \
+      --load-privkey $out/ca.key \
+      --template ca.template     \
+      --outfile $out/ca.crt
+
+    # generate server keypair
+    certtool                    \
+      --generate-privkey        \
+      --key-type rsa            \
+      --sec-param High          \
+      --outfile $out/server.key
+    certtool                            \
+      --generate-certificate            \
+      --load-privkey $out/server.key    \
+      --load-ca-privkey $out/ca.key     \
+      --load-ca-certificate $out/ca.crt \
+      --template server.template        \
+      --outfile $out/server.crt
+  '';
+
+  example-good-cert = makeCert
+    { caName = "Example good CA";
+      domain = "good.example.com";
+    };
+
+  example-bad-cert = makeCert
+    { caName = "Unknown CA";
+      domain = "bad.example.com";
+    };
+
+in
+
+{
+  name = "custom-ca";
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+  enableOCR = true;
+
+  machine = { pkgs, ... }:
+    { imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+      # chromium-based browsers refuse to run as root
+      test-support.displayManager.auto.user = "alice";
+      # browsers may hang with the default memory
+      virtualisation.memorySize = "500";
+
+      networking.hosts."127.0.0.1" = [ "good.example.com" "bad.example.com" ];
+      security.pki.certificateFiles = [ "${example-good-cert}/ca.crt" ];
+
+      services.nginx.enable = true;
+      services.nginx.virtualHosts."good.example.com" =
+        { onlySSL = true;
+          sslCertificate = "${example-good-cert}/server.crt";
+          sslCertificateKey = "${example-good-cert}/server.key";
+          locations."/".extraConfig = "return 200 'It works!';";
+        };
+      services.nginx.virtualHosts."bad.example.com" =
+        { onlySSL = true;
+          sslCertificate = "${example-bad-cert}/server.crt";
+          sslCertificateKey = "${example-bad-cert}/server.key";
+          locations."/".extraConfig = "return 200 'It does not work!';";
+        };
+
+      environment.systemPackages = with pkgs;
+        [ xdotool firefox chromium falkon midori ];
+    };
+
+  testScript = ''
+    def execute_as(user: str, cmd: str) -> Tuple[int, str]:
+        """
+        Run a shell command as a specific user.
+        """
+        return machine.execute(f"sudo -u {user} {cmd}")
+
+
+    def wait_for_window_as(user: str, cls: str) -> None:
+        """
+        Wait until a X11 window of a given user appears.
+        """
+
+        def window_is_visible(last_try: bool) -> bool:
+            ret, stdout = execute_as(user, f"xdotool search --onlyvisible --class {cls}")
+            if last_try:
+                machine.log(f"Last chance to match {cls} on the window list")
+            return ret == 0
+
+        with machine.nested("Waiting for a window to appear"):
+            retry(window_is_visible)
+
+
+    machine.start()
+
+    with subtest("Good certificate is trusted in curl"):
+        machine.wait_for_unit("nginx")
+        machine.wait_for_open_port(443)
+        machine.succeed("curl -fv https://good.example.com")
+
+    with subtest("Unknown CA is untrusted in curl"):
+        machine.fail("curl -fv https://bad.example.com")
+
+    browsers = ["firefox", "chromium", "falkon", "midori"]
+    errors = ["Security Risk", "not private", "Certificate Error", "Security"]
+
+    machine.wait_for_x()
+    for browser, error in zip(browsers, errors):
+        with subtest("Good certificate is trusted in " + browser):
+            execute_as(
+                "alice", f"env P11_KIT_DEBUG=trust {browser} https://good.example.com & >&2"
+            )
+            wait_for_window_as("alice", browser)
+            machine.wait_for_text("It works!")
+            machine.screenshot("good" + browser)
+            execute_as("alice", "xdotool key ctrl+w")  # close tab
+
+        with subtest("Unknown CA is untrusted in " + browser):
+            execute_as("alice", f"{browser} https://bad.example.com & >&2")
+            machine.wait_for_text(error)
+            machine.screenshot("bad" + browser)
+            machine.succeed("pkill " + browser)
+  '';
+})
diff --git a/nixos/tests/deluge.nix b/nixos/tests/deluge.nix
index 3cf179a382164..300bc0a1157b3 100644
--- a/nixos/tests/deluge.nix
+++ b/nixos/tests/deluge.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "deluge";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ flokli ];
   };
 
diff --git a/nixos/tests/dnscrypt-proxy2.nix b/nixos/tests/dnscrypt-proxy2.nix
index b614d912a9f4e..1ba5d983e9b92 100644
--- a/nixos/tests/dnscrypt-proxy2.nix
+++ b/nixos/tests/dnscrypt-proxy2.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "dnscrypt-proxy2";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ joachifm ];
   };
 
diff --git a/nixos/tests/dnscrypt-wrapper/default.nix b/nixos/tests/dnscrypt-wrapper/default.nix
index 1dc925f4de7ab..1bdd064e1130c 100644
--- a/nixos/tests/dnscrypt-wrapper/default.nix
+++ b/nixos/tests/dnscrypt-wrapper/default.nix
@@ -1,6 +1,6 @@
 import ../make-test-python.nix ({ pkgs, ... }: {
   name = "dnscrypt-wrapper";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ rnhmjoj ];
   };
 
@@ -31,6 +31,7 @@ import ../make-test-python.nix ({ pkgs, ... }: {
 
     client = { lib, ... }:
       { services.dnscrypt-proxy2.enable = true;
+        services.dnscrypt-proxy2.upstreamDefaults = false;
         services.dnscrypt-proxy2.settings = {
           server_names = [ "server" ];
           static.server.stamp = "sdns://AQAAAAAAAAAAEDE5Mi4xNjguMS4xOjUzNTMgFEHYOv0SCKSuqR5CDYa7-58cCBuXO2_5uTSVU9wNQF0WMi5kbnNjcnlwdC1jZXJ0LnNlcnZlcg";
diff --git a/nixos/tests/docker-edge.nix b/nixos/tests/docker-edge.nix
index 96de885a554ab..c6a1a08301890 100644
--- a/nixos/tests/docker-edge.nix
+++ b/nixos/tests/docker-edge.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus offline ];
   };
 
@@ -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-edge.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-edge.version} | wc -l) = "4" ]')
   '';
 })
diff --git a/nixos/tests/docker-registry.nix b/nixos/tests/docker-registry.nix
index 2928fd8141a43..1d449db45191f 100644
--- a/nixos/tests/docker-registry.nix
+++ b/nixos/tests/docker-registry.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker-registry";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ globin ma27 ironpinguin ];
   };
 
diff --git a/nixos/tests/docker-tools-cross.nix b/nixos/tests/docker-tools-cross.nix
new file mode 100644
index 0000000000000..a7a6a31475d67
--- /dev/null
+++ b/nixos/tests/docker-tools-cross.nix
@@ -0,0 +1,76 @@
+# Not everyone has a suitable remote builder set up, so the cross-compilation
+# tests that _include_ running the result are separate. That way, most people
+# can run the majority of the test suite without the extra setup.
+
+
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+
+  remoteSystem =
+    if pkgs.system == "aarch64-linux"
+    then "x86_64-linux"
+    else "aarch64-linux";
+
+  remoteCrossPkgs = import ../.. /*nixpkgs*/ {
+    # NOTE: This is the machine that runs the build -  local from the
+    #       'perspective' of the build script.
+    localSystem = remoteSystem;
+
+    # NOTE: Since this file can't control where the test will be _run_ we don't
+    #       cross-compile _to_ a different system but _from_ a different system
+    crossSystem = pkgs.system;
+  };
+
+  hello1 = remoteCrossPkgs.dockerTools.buildImage {
+    name = "hello1";
+    tag = "latest";
+    contents = remoteCrossPkgs.hello;
+  };
+
+  hello2 = remoteCrossPkgs.dockerTools.buildLayeredImage {
+    name = "hello2";
+    tag = "latest";
+    contents = remoteCrossPkgs.hello;
+  };
+
+in {
+  name = "docker-tools";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ roberth ];
+  };
+
+  nodes = {
+    docker = { ... }: {
+      virtualisation = {
+        diskSize = 2048;
+        docker.enable = true;
+      };
+    };
+  };
+
+  testScript = ''
+    docker.wait_for_unit("sockets.target")
+
+    with subtest("Ensure cross compiled buildImage image can run."):
+        docker.succeed(
+            "docker load --input='${hello1}'"
+        )
+        assert "Hello, world!" in docker.succeed(
+            "docker run --rm ${hello1.imageName} hello",
+        )
+        docker.succeed(
+            "docker rmi ${hello1.imageName}",
+        )
+
+    with subtest("Ensure cross compiled buildLayeredImage image can run."):
+        docker.succeed(
+            "docker load --input='${hello2}'"
+        )
+        assert "Hello, world!" in docker.succeed(
+            "docker run --rm ${hello2.imageName} hello",
+        )
+        docker.succeed(
+            "docker rmi ${hello2.imageName}",
+        )
+  '';
+})
diff --git a/nixos/tests/docker-tools-overlay.nix b/nixos/tests/docker-tools-overlay.nix
index 1a0e0ea67750a..98eb728661565 100644
--- a/nixos/tests/docker-tools-overlay.nix
+++ b/nixos/tests/docker-tools-overlay.nix
@@ -3,7 +3,7 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "docker-tools-overlay";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 ];
   };
 
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index edb9aec62db30..6638ec4927ce1 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "docker-tools";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 ];
   };
 
@@ -115,9 +115,10 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker load --input='${examples.nginx}'",
             "docker run --name nginx -d -p 8000:80 ${examples.nginx.imageName}",
         )
-        docker.wait_until_succeeds("curl http://localhost:8000/")
+        docker.wait_until_succeeds("curl -f http://localhost:8000/")
         docker.succeed(
-            "docker rm --force nginx", "docker rmi '${examples.nginx.imageName}'",
+            "docker rm --force nginx",
+            "docker rmi '${examples.nginx.imageName}'",
         )
 
     with subtest("A pulled image can be used as base image"):
@@ -234,5 +235,24 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker run --rm file-in-store nix-store --verify --check-contents",
             "docker run --rm file-in-store |& grep 'some data'",
         )
+
+    with subtest("Ensure cross compiled image can be loaded and has correct arch."):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.cross}'",
+        )
+        assert (
+            docker.succeed(
+                "docker inspect ${pkgs.dockerTools.examples.cross.imageName} "
+                + "| ${pkgs.jq}/bin/jq -r .[].Architecture"
+            ).strip()
+            == "${if pkgs.system == "aarch64-linux" then "amd64" else "arm64"}"
+        )
+
+    with subtest("buildLayeredImage doesn't dereference /nix/store symlink layers"):
+        docker.succeed(
+            "docker load --input='${examples.layeredStoreSymlink}'",
+            "docker run --rm ${examples.layeredStoreSymlink.imageName} bash -c 'test -L ${examples.layeredStoreSymlink.passthru.symlink}'",
+            "docker rmi ${examples.layeredStoreSymlink.imageName}",
+        )
   '';
 })
diff --git a/nixos/tests/docker.nix b/nixos/tests/docker.nix
index a4a61468f33dc..58e33535ed31e 100644
--- a/nixos/tests/docker.nix
+++ b/nixos/tests/docker.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus offline ];
   };
 
diff --git a/nixos/tests/documize.nix b/nixos/tests/documize.nix
index 3be20a780d318..d5a77ffcd4f26 100644
--- a/nixos/tests/documize.nix
+++ b/nixos/tests/documize.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "documize";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 ];
   };
 
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
index 58069366ca36e..40475d789d47a 100644
--- a/nixos/tests/dokuwiki.nix
+++ b/nixos/tests/dokuwiki.nix
@@ -32,7 +32,7 @@ let
 
 in {
   name = "dokuwiki";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ _1000101 ];
   };
   machine = { ... }: {
diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix
index bcbe234fd8052..1129e3b45d9d6 100644
--- a/nixos/tests/dovecot.nix
+++ b/nixos/tests/dovecot.nix
@@ -4,8 +4,11 @@ import ./make-test-python.nix {
   machine = { pkgs, ... }: {
     imports = [ common/user-account.nix ];
     services.postfix.enable = true;
-    services.dovecot2.enable = true;
-    services.dovecot2.protocols = [ "imap" "pop3" ];
+    services.dovecot2 = {
+      enable = true;
+      protocols = [ "imap" "pop3" ];
+      modules = [ pkgs.dovecot_pigeonhole ];
+    };
     environment.systemPackages = let
       sendTestMail = pkgs.writeScriptBin "send-testmail" ''
         #!${pkgs.runtimeShell}
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index 7e87197ed9f3d..8488c97c01e8c 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -12,7 +12,7 @@ let
   mkElkTest = name : elk :
     import ./make-test-python.nix ({
     inherit name;
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ eelco offline basvandijk ];
     };
     nodes = {
diff --git a/nixos/tests/emacs-daemon.nix b/nixos/tests/emacs-daemon.nix
index b89d9b1bde691..58bcd095990ad 100644
--- a/nixos/tests/emacs-daemon.nix
+++ b/nixos/tests/emacs-daemon.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "emacs-daemon";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ];
   };
 
diff --git a/nixos/tests/engelsystem.nix b/nixos/tests/engelsystem.nix
index 39c10718093f6..7be3b8a5a1fe2 100644
--- a/nixos/tests/engelsystem.nix
+++ b/nixos/tests/engelsystem.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix (
   { pkgs, lib, ... }:
   {
     name = "engelsystem";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ talyz ];
     };
 
diff --git a/nixos/tests/enlightenment.nix b/nixos/tests/enlightenment.nix
index 0132b98b1cbb1..cc1da649d493e 100644
--- a/nixos/tests/enlightenment.nix
+++ b/nixos/tests/enlightenment.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 {
   name = "enlightenment";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ romildo ];
   };
 
diff --git a/nixos/tests/env.nix b/nixos/tests/env.nix
index e603338e489b9..fc96ace6b2d2a 100644
--- a/nixos/tests/env.nix
+++ b/nixos/tests/env.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "environment";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
diff --git a/nixos/tests/ergo.nix b/nixos/tests/ergo.nix
index 8cdbbf62a9564..b49e0c9dfed73 100644
--- a/nixos/tests/ergo.nix
+++ b/nixos/tests/ergo.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "ergo";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
diff --git a/nixos/tests/etcd-cluster.nix b/nixos/tests/etcd-cluster.nix
index 19c5d9158236b..410cb654794f2 100644
--- a/nixos/tests/etcd-cluster.nix
+++ b/nixos/tests/etcd-cluster.nix
@@ -97,7 +97,7 @@ import ./make-test-python.nix ({ pkgs, ... } : let
 in {
   name = "etcd";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ offline ];
   };
 
diff --git a/nixos/tests/etcd.nix b/nixos/tests/etcd.nix
index 8427243438411..702bbb668f57f 100644
--- a/nixos/tests/etcd.nix
+++ b/nixos/tests/etcd.nix
@@ -3,7 +3,7 @@
 import ./make-test-python.nix ({ pkgs, ... } : {
   name = "etcd";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ offline ];
   };
 
diff --git a/nixos/tests/etesync-dav.nix b/nixos/tests/etesync-dav.nix
new file mode 100644
index 0000000000000..da5c056f53497
--- /dev/null
+++ b/nixos/tests/etesync-dav.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+
+  name = "etesync-dav";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ _3699n ];
+  };
+
+  machine = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curl pkgs.etesync-dav ];
+  };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("etesync-dav --version")
+      machine.execute("etesync-dav &")
+      machine.wait_for_open_port(37358)
+      with subtest("Check that the web interface is accessible"):
+          assert "Add User" in machine.succeed("curl -s http://localhost:37358/.web/add/")
+    '';
+})
diff --git a/nixos/tests/fcitx/config b/nixos/tests/fcitx/config
new file mode 100644
index 0000000000000..169768994e280
--- /dev/null
+++ b/nixos/tests/fcitx/config
@@ -0,0 +1,12 @@
+[Hotkey]
+SwitchKey=Disabled
+IMSwitchHotkey=ALT_SHIFT
+TimeInterval=240
+
+[Program]
+DelayStart=5
+
+[Output]
+
+[Appearance]
+
diff --git a/nixos/tests/fcitx/default.nix b/nixos/tests/fcitx/default.nix
new file mode 100644
index 0000000000000..cbeb95d33b0cb
--- /dev/null
+++ b/nixos/tests/fcitx/default.nix
@@ -0,0 +1,142 @@
+import ../make-test-python.nix (
+  {
+    pkgs, ...
+  }:
+    # copy_from_host works only for store paths
+    rec {
+        name = "fcitx";
+        machine =
+        {
+          pkgs,
+          ...
+        }:
+          {
+            virtualisation.memorySize = 1024;
+
+            imports = [
+              ../common/user-account.nix
+            ];
+
+            environment.systemPackages = [
+              # To avoid clashing with xfce4-terminal
+              pkgs.alacritty
+            ];
+
+
+            services.xserver =
+            {
+              enable = true;
+
+              displayManager = {
+                lightdm.enable = true;
+                autoLogin = {
+                  enable = true;
+                  user = "alice";
+                };
+              };
+
+              desktopManager.xfce.enable = true;
+            };
+
+            i18n = {
+              inputMethod = {
+                enabled = "fcitx";
+                fcitx.engines = [
+                  pkgs.fcitx-engines.m17n
+                  pkgs.fcitx-engines.table-extra
+                ];
+              };
+            };
+          }
+        ;
+
+        testScript = { nodes, ... }:
+        let
+            user = nodes.machine.config.users.users.alice;
+            userName      = user.name;
+            userHome      = user.home;
+            xauth         = "${userHome}/.Xauthority";
+            fcitx_confdir = "${userHome}/.config/fcitx";
+        in
+        ''
+            # We need config files before login session
+            # So copy first thing
+
+            # Point and click would be expensive,
+            # So configure using files
+            machine.copy_from_host(
+                "${./profile}",
+                "${fcitx_confdir}/profile",
+            )
+            machine.copy_from_host(
+                "${./config}",
+                "${fcitx_confdir}/config",
+            )
+
+            start_all()
+
+            machine.wait_for_file("${xauth}")
+            machine.succeed("xauth merge ${xauth}")
+
+            machine.sleep(5)
+
+            machine.succeed("su - ${userName} -c 'alacritty&'")
+            machine.succeed("su - ${userName} -c 'fcitx&'")
+            machine.sleep(10)
+
+            ### Type on terminal
+            machine.send_chars("echo ")
+            machine.sleep(1)
+
+            ### Start fcitx Unicode input
+            machine.send_key("ctrl-alt-shift-u")
+            machine.sleep(5)
+            machine.sleep(1)
+
+            ### Search for smiling face
+            machine.send_chars("smil")
+            machine.sleep(1)
+
+            ### Navigate to the second one
+            machine.send_key("tab")
+            machine.sleep(1)
+
+            ### Choose it
+            machine.send_key("\n")
+            machine.sleep(1)
+
+            ### Start fcitx language input
+            machine.send_key("ctrl-spc")
+            machine.sleep(1)
+
+            ### Default zhengma, enter 一下
+            machine.send_chars("a2")
+            machine.sleep(1)
+
+            ### Switch to Harvard Kyoto
+            machine.send_key("alt-shift")
+            machine.sleep(1)
+
+            ### Enter क
+            machine.send_chars("ka ")
+            machine.sleep(1)
+
+            machine.send_key("alt-shift")
+            machine.sleep(1)
+
+            ### Turn off Fcitx
+            machine.send_key("ctrl-spc")
+            machine.sleep(1)
+
+            ### Redirect typed characters to a file
+            machine.send_chars(" > fcitx_test.out\n")
+            machine.sleep(1)
+            machine.screenshot("terminal_chars")
+
+            ### Verify that file contents are as expected
+            file_content = machine.succeed("cat ${userHome}/fcitx_test.out")
+            assert file_content == "☺一下क\n"
+            ''
+    ;
+  }
+)
diff --git a/nixos/tests/fcitx/profile b/nixos/tests/fcitx/profile
new file mode 100644
index 0000000000000..77497a1496bd1
--- /dev/null
+++ b/nixos/tests/fcitx/profile
@@ -0,0 +1,4 @@
+[Profile]
+IMName=zhengma-large
+EnabledIMList=fcitx-keyboard-us:True,zhengma-large:True,m17n_sa_harvard-kyoto:True
+PreeditStringInClientWindow=False
diff --git a/nixos/tests/fenics.nix b/nixos/tests/fenics.nix
index 7252d19e4e654..56f09d6a27e40 100644
--- a/nixos/tests/fenics.nix
+++ b/nixos/tests/fenics.nix
@@ -29,7 +29,7 @@ in
 {
   name = "fenics";
   meta = {
-    maintainers = with pkgs.stdenv.lib.maintainers; [ knedlsepp ];
+    maintainers = with pkgs.lib.maintainers; [ knedlsepp ];
   };
 
   nodes = {
diff --git a/nixos/tests/ferm.nix b/nixos/tests/ferm.nix
index a73c9ce739cf1..be43877445ebf 100644
--- a/nixos/tests/ferm.nix
+++ b/nixos/tests/ferm.nix
@@ -1,7 +1,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "ferm";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mic92 ];
   };
 
@@ -56,6 +56,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       start_all()
 
       client.wait_for_unit("network-online.target")
+      server.wait_for_unit("network-online.target")
       server.wait_for_unit("ferm.service")
       server.wait_for_unit("nginx.service")
       server.wait_until_succeeds("ss -ntl | grep -q 80")
diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix
index 7071baceba739..4262f5443bf89 100644
--- a/nixos/tests/firefox.nix
+++ b/nixos/tests/firefox.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, esr ? false, ... }: {
   name = "firefox";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco shlevy ];
   };
 
@@ -11,26 +11,105 @@ import ./make-test-python.nix ({ pkgs, esr ? false, ... }: {
       environment.systemPackages =
         (if esr then [ pkgs.firefox-esr ] else [ pkgs.firefox ])
         ++ [ pkgs.xdotool ];
+
+      # Need some more memory to record audio.
+      virtualisation.memorySize = "500";
+
+      # Create a virtual sound device, with mixing
+      # and all, for recording audio.
+      boot.kernelModules = [ "snd-aloop" ];
+      sound.enable = true;
+      sound.extraConfig = ''
+        pcm.!default {
+          type plug
+          slave.pcm pcm.dmixer
+        }
+        pcm.dmixer {
+          type dmix
+          ipc_key 1
+          slave {
+            pcm "hw:Loopback,0,0"
+            rate 48000
+            periods 128
+            period_time 0
+            period_size 1024
+            buffer_size 8192
+          }
+        }
+        pcm.recorder {
+          type hw
+          card "Loopback"
+          device 1
+          subdevice 0
+        }
+      '';
+
+      systemd.services.audio-recorder = {
+        description = "Record NixOS test audio to /tmp/record.wav";
+        script = "${pkgs.alsaUtils}/bin/arecord -D recorder -f S16_LE -r48000 /tmp/record.wav";
+      };
+
     };
 
   testScript = ''
+      from contextlib import contextmanager
+
+
+      @contextmanager
+      def audio_recording(machine: Machine) -> None:
+          """
+          Perform actions while recording the
+          machine audio output.
+          """
+          machine.systemctl("start audio-recorder")
+          yield
+          machine.systemctl("stop audio-recorder")
+
+
+      def wait_for_sound(machine: Machine) -> None:
+          """
+          Wait until any sound has been emitted.
+          """
+          machine.wait_for_file("/tmp/record.wav")
+          while True:
+              # Get at most 2M of the recording
+              machine.execute("tail -c 2M /tmp/record.wav > /tmp/last")
+              # Get the exact size
+              size = int(machine.succeed("stat -c '%s' /tmp/last").strip())
+              # Compare it against /dev/zero using `cmp` (skipping 50B of WAVE header).
+              # If some non-NULL bytes are found it returns 1.
+              status, output = machine.execute(
+                  f"cmp -i 50 -n {size - 50} /tmp/last /dev/zero 2>&1"
+              )
+              if status == 1:
+                  break
+              machine.sleep(2)
+
+
       machine.wait_for_x()
 
-      with subtest("wait until Firefox has finished loading the Valgrind docs page"):
+      with subtest("Wait until Firefox has finished loading the Valgrind docs page"):
           machine.execute(
               "xterm -e 'firefox file://${pkgs.valgrind.doc}/share/doc/valgrind/html/index.html' &"
           )
           machine.wait_for_window("Valgrind")
           machine.sleep(40)
 
+      with subtest("Check whether Firefox can play sound"):
+          with audio_recording(machine):
+              machine.succeed(
+                  "firefox file://${pkgs.sound-theme-freedesktop}/share/sounds/freedesktop/stereo/phone-incoming-call.oga &"
+              )
+              wait_for_sound(machine)
+          machine.copy_from_vm("/tmp/record.wav")
+
+      with subtest("Close sound test tab"):
+          machine.execute("xdotool key ctrl+w")
+
       with subtest("Close default browser prompt"):
           machine.execute("xdotool key space")
 
-      with subtest("Hide default browser window"):
-          machine.sleep(2)
-          machine.execute("xdotool key F12")
-
-      with subtest("wait until Firefox draws the developer tool panel"):
+      with subtest("Wait until Firefox draws the developer tool panel"):
           machine.sleep(10)
           machine.succeed("xwininfo -root -tree | grep Valgrind")
           machine.screenshot("screen")
diff --git a/nixos/tests/firejail.nix b/nixos/tests/firejail.nix
index a723cb01664f3..6c42c37b2813a 100644
--- a/nixos/tests/firejail.nix
+++ b/nixos/tests/firejail.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "firejail";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ sgo ];
   };
 
@@ -11,6 +11,10 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       enable = true;
       wrappedBinaries = {
         bash-jailed  = "${pkgs.bash}/bin/bash";
+        bash-jailed2  = {
+          executable = "${pkgs.bash}/bin/bash";
+          extraArgs = [ "--private=~/firejail-home" ];
+        };
       };
     };
 
@@ -53,6 +57,11 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     )
     machine.fail("sudo -u alice bash-jailed -c 'cat ~/my-secrets/secret' | grep -q s3cret")
 
+    # Test extraArgs
+    machine.succeed("sudo -u alice mkdir /home/alice/firejail-home")
+    machine.succeed("sudo -u alice bash-jailed2 -c 'echo test > /home/alice/foo'")
+    machine.fail("sudo -u alice cat /home/alice/foo")
+    machine.succeed("sudo -u alice cat /home/alice/firejail-home/foo | grep test")
 
     # Test path acl with firejail executable
     machine.succeed("sudo -u alice firejail -- bash -c 'cat ~/public' | grep -q publ1c")
diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix
index 09a1fef852e64..5c434c1cb6d68 100644
--- a/nixos/tests/firewall.nix
+++ b/nixos/tests/firewall.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ( { pkgs, ... } : {
   name = "firewall";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/freeswitch.nix b/nixos/tests/freeswitch.nix
index 349d0e7bc6f0d..bcc6a9cb3586c 100644
--- a/nixos/tests/freeswitch.nix
+++ b/nixos/tests/freeswitch.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "freeswitch";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ misuzu ];
   };
   nodes = {
diff --git a/nixos/tests/fsck.nix b/nixos/tests/fsck.nix
index e522419fde2b9..5453f3bc48b59 100644
--- a/nixos/tests/fsck.nix
+++ b/nixos/tests/fsck.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix {
   machine = { lib, ... }: {
     virtualisation.emptyDiskImages = [ 1 ];
 
-    fileSystems = lib.mkVMOverride {
+    virtualisation.fileSystems = {
       "/mnt" = {
         device = "/dev/vdb";
         fsType = "ext4";
diff --git a/nixos/tests/ft2-clone.nix b/nixos/tests/ft2-clone.nix
new file mode 100644
index 0000000000000..c877054234ec8
--- /dev/null
+++ b/nixos/tests/ft2-clone.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "ft2-clone";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.ft2-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("ft2-clone &")
+
+      machine.wait_for_window(r"Fasttracker")
+      machine.sleep(5)
+      # One of the few words that actually get recognized
+      if "Songlen" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/gerrit.nix b/nixos/tests/gerrit.nix
index 6cee64a20095a..b6b6486fae86c 100644
--- a/nixos/tests/gerrit.nix
+++ b/nixos/tests/gerrit.nix
@@ -9,7 +9,7 @@ let
 in {
   name = "gerrit";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ flokli zimbatm ];
   };
 
diff --git a/nixos/tests/git/hub.nix b/nixos/tests/git/hub.nix
new file mode 100644
index 0000000000000..4f3189861a004
--- /dev/null
+++ b/nixos/tests/git/hub.nix
@@ -0,0 +1,17 @@
+import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "hub";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  nodes.hub = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.hub ];
+    };
+
+  testScript =
+    ''
+      assert "git version ${pkgs.git.version}\nhub version ${pkgs.hub.version}\n" in hub.succeed("hub version")
+      assert "These GitHub commands are provided by hub" in hub.succeed("hub help")
+    '';
+})
diff --git a/nixos/tests/gitdaemon.nix b/nixos/tests/gitdaemon.nix
index c4a707943ef1b..d0156fb9a49fb 100644
--- a/nixos/tests/gitdaemon.nix
+++ b/nixos/tests/gitdaemon.nix
@@ -7,7 +7,7 @@ let
 in {
   name = "gitdaemon";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ tilpner ];
   };
 
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index aaed2486421f9..1fb27593f0569 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -14,6 +14,7 @@ let
 
     nodes = {
       server = { config, pkgs, ... }: {
+        virtualisation.memorySize = 2048;
         services.gitea = {
           enable = true;
           database = { inherit type; };
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index 7e4e8bcef92dc..ba085338944a0 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -5,7 +5,7 @@ let
 in
 import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
   name = "gitlab";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ globin ];
   };
 
@@ -33,9 +33,9 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
         initialRootPasswordFile = pkgs.writeText "rootPassword" initialRootPassword;
         smtp.enable = true;
         secrets = {
-          secretFile = pkgs.writeText "secret" "Aig5zaic";
-          otpFile = pkgs.writeText "otpsecret" "Riew9mue";
-          dbFile = pkgs.writeText "dbsecret" "we2quaeZ";
+          secretFile = pkgs.writeText "secret" "r8X9keSKynU7p4aKlh4GO1Bo77g5a7vj";
+          otpFile = pkgs.writeText "otpsecret" "Zu5hGx3YvQx40DvI8WoZJQpX2paSDOlG";
+          dbFile = pkgs.writeText "dbsecret" "lsGltKWTejOf6JxCVa7nLDenzkO9wPLR";
           jwsFile = pkgs.runCommand "oidcKeyBase" {} "${pkgs.openssl}/bin/openssl genrsa 2048 > $out";
         };
       };
diff --git a/nixos/tests/gitolite-fcgiwrap.nix b/nixos/tests/gitolite-fcgiwrap.nix
index 414b7d6fe7efe..fc9b214b762ee 100644
--- a/nixos/tests/gitolite-fcgiwrap.nix
+++ b/nixos/tests/gitolite-fcgiwrap.nix
@@ -13,7 +13,7 @@ import ./make-test-python.nix (
       {
         name = "gitolite-fcgiwrap";
 
-        meta = with pkgs.stdenv.lib.maintainers; {
+        meta = with pkgs.lib.maintainers; {
           maintainers = [ bbigras ];
         };
 
diff --git a/nixos/tests/gitolite.nix b/nixos/tests/gitolite.nix
index a928645bd80f8..128677cebde3a 100644
--- a/nixos/tests/gitolite.nix
+++ b/nixos/tests/gitolite.nix
@@ -51,7 +51,7 @@ in
 {
   name = "gitolite";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bjornfor ];
   };
 
diff --git a/nixos/tests/glusterfs.nix b/nixos/tests/glusterfs.nix
index cb07bc09511dd..ef09264a02167 100644
--- a/nixos/tests/glusterfs.nix
+++ b/nixos/tests/glusterfs.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({pkgs, lib, ...}:
 let
   client = { pkgs, ... } : {
     environment.systemPackages = [ pkgs.glusterfs ];
-    fileSystems = pkgs.lib.mkVMOverride
+    virtualisation.fileSystems =
       { "/gluster" =
           { device = "server1:/gv0";
             fsType = "glusterfs";
@@ -22,7 +22,7 @@ let
 
     virtualisation.emptyDiskImages = [ 1024 ];
 
-    fileSystems = pkgs.lib.mkVMOverride
+    virtualisation.fileSystems =
       { "/data" =
           { device = "/dev/disk/by-label/data";
             fsType = "ext4";
diff --git a/nixos/tests/go-neb.nix b/nixos/tests/go-neb.nix
index d9e5db0b4a534..f8801ff68d64d 100644
--- a/nixos/tests/go-neb.nix
+++ b/nixos/tests/go-neb.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "go-neb";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hexa maralorn ];
   };
 
@@ -34,7 +34,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     start_all()
     server.wait_for_unit("go-neb.service")
     server.wait_until_succeeds(
-        "curl -L http://localhost:4050/services/hooks/d2lraXBlZGlhX3NlcnZpY2U"
+        "curl -fL 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/gocd-agent.nix b/nixos/tests/gocd-agent.nix
index 5b630a40736e9..75edf43ee295b 100644
--- a/nixos/tests/gocd-agent.nix
+++ b/nixos/tests/gocd-agent.nix
@@ -11,7 +11,7 @@ in
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "gocd-agent";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ grahamc swarren83 ];
 
     # gocd agent needs to register with the autoregister key created on first server startup,
diff --git a/nixos/tests/gocd-server.nix b/nixos/tests/gocd-server.nix
index 20faf85a1ccd8..aff651c5278fb 100644
--- a/nixos/tests/gocd-server.nix
+++ b/nixos/tests/gocd-server.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
 {
   name = "gocd-server";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ swarren83 ];
   };
 
diff --git a/nixos/tests/google-oslogin/default.nix b/nixos/tests/google-oslogin/default.nix
index 97783c81f3972..dea660ed05a48 100644
--- a/nixos/tests/google-oslogin/default.nix
+++ b/nixos/tests/google-oslogin/default.nix
@@ -11,7 +11,7 @@ let
     '';
 in {
   name = "google-oslogin";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ adisbladis flokli ];
   };
 
diff --git a/nixos/tests/gotify-server.nix b/nixos/tests/gotify-server.nix
index c0b8ba43548a8..051666fbe72e7 100644
--- a/nixos/tests/gotify-server.nix
+++ b/nixos/tests/gotify-server.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "gotify-server";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 ];
   };
 
diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana.nix
index 4b453ece7f1ef..4ba091b893f42 100644
--- a/nixos/tests/grafana.nix
+++ b/nixos/tests/grafana.nix
@@ -17,6 +17,10 @@ let
   };
 
   extraNodeConfs = {
+    declarativePlugins = {
+      services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
+    };
+
     postgresql = {
       services.grafana.database = {
         host = "127.0.0.1:5432";
@@ -52,7 +56,7 @@ let
     nameValuePair dbName (mkMerge [
     baseGrafanaConf
     (extraNodeConfs.${dbName} or {})
-  ])) [ "sqlite" "postgresql" "mysql" ]);
+  ])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]);
 
 in {
   name = "grafana";
@@ -66,6 +70,14 @@ in {
   testScript = ''
     start_all()
 
+    with subtest("Declarative plugins installed"):
+        declarativePlugins.wait_for_unit("grafana.service")
+        declarativePlugins.wait_for_open_port(3000)
+        declarativePlugins.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/plugins | grep -q grafana-clock-panel"
+        )
+        declarativePlugins.shutdown()
+
     with subtest("Successful API query as admin user with sqlite db"):
         sqlite.wait_for_unit("grafana.service")
         sqlite.wait_for_open_port(3000)
diff --git a/nixos/tests/grocy.nix b/nixos/tests/grocy.nix
index 7fa479ed2c42e..220c55b1f6346 100644
--- a/nixos/tests/grocy.nix
+++ b/nixos/tests/grocy.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "grocy";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 ];
   };
 
diff --git a/nixos/tests/gvisor.nix b/nixos/tests/gvisor.nix
index 4d68a1d8a5f89..77ff29341bed2 100644
--- a/nixos/tests/gvisor.nix
+++ b/nixos/tests/gvisor.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "gvisor";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ andrew-d ];
   };
 
diff --git a/nixos/tests/hadoop/hdfs.nix b/nixos/tests/hadoop/hdfs.nix
index 85aaab34b158f..f1f98ed42eb31 100644
--- a/nixos/tests/hadoop/hdfs.nix
+++ b/nixos/tests/hadoop/hdfs.nix
@@ -48,7 +48,7 @@ import ../make-test-python.nix ({...}: {
     datanode.wait_for_open_port(9866)
     datanode.wait_for_open_port(9867)
 
-    namenode.succeed("curl http://namenode:9870")
-    datanode.succeed("curl http://datanode:9864")
+    namenode.succeed("curl -f http://namenode:9870")
+    datanode.succeed("curl -f http://datanode:9864")
   '';
 })
diff --git a/nixos/tests/hadoop/yarn.nix b/nixos/tests/hadoop/yarn.nix
index 2264ecaff1555..01077245d3973 100644
--- a/nixos/tests/hadoop/yarn.nix
+++ b/nixos/tests/hadoop/yarn.nix
@@ -40,7 +40,7 @@ import ../make-test-python.nix ({...}: {
     nodemanager.wait_for_open_port(8042)
     nodemanager.wait_for_open_port(8041)
 
-    resourcemanager.succeed("curl http://localhost:8088")
-    nodemanager.succeed("curl http://localhost:8042")
+    resourcemanager.succeed("curl -f http://localhost:8088")
+    nodemanager.succeed("curl -f http://localhost:8042")
   '';
 })
diff --git a/nixos/tests/haka.nix b/nixos/tests/haka.nix
index 3ca19cb0971c7..dd65a6bcf115a 100644
--- a/nixos/tests/haka.nix
+++ b/nixos/tests/haka.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "haka";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ tvestelind ];
   };
 
diff --git a/nixos/tests/handbrake.nix b/nixos/tests/handbrake.nix
index e5fb6b269b197..226dc8b2aa8a8 100644
--- a/nixos/tests/handbrake.nix
+++ b/nixos/tests/handbrake.nix
@@ -9,7 +9,7 @@ in {
   name = "handbrake";
 
   meta = {
-    maintainers = with pkgs.stdenv.lib.maintainers; [ danieldk ];
+    maintainers = with pkgs.lib.maintainers; [ danieldk ];
   };
 
   machine = { pkgs, ... }: {
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index ffb77c052a24b..2c3878131b68b 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -39,9 +39,9 @@ import ./make-test-python.nix ({ pkgs, ...}: {
     machine.wait_for_unit("multi-user.target")
     machine.wait_for_unit("haproxy.service")
     machine.wait_for_unit("httpd.service")
-    assert "We are all good!" in machine.succeed("curl -k http://localhost:80/index.txt")
+    assert "We are all good!" in machine.succeed("curl -fk http://localhost:80/index.txt")
     assert "haproxy_process_pool_allocated_bytes" in machine.succeed(
-        "curl -k http://localhost:80/metrics"
+        "curl -fk http://localhost:80/metrics"
     )
 
     with subtest("reload"):
@@ -49,7 +49,7 @@ import ./make-test-python.nix ({ pkgs, ...}: {
         # 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"
+            "curl -fk http://localhost:80/index.txt"
         )
   '';
 })
diff --git a/nixos/tests/hardened.nix b/nixos/tests/hardened.nix
index ab5fa609e0725..0c26eaa310d49 100644
--- a/nixos/tests/hardened.nix
+++ b/nixos/tests/hardened.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... } : {
   name = "hardened";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ joachifm ];
   };
 
@@ -18,7 +18,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... } : {
       boot.initrd.postDeviceCommands = ''
         ${pkgs.dosfstools}/bin/mkfs.vfat -n EFISYS /dev/vdb
       '';
-      fileSystems = lib.mkVMOverride {
+      virtualisation.fileSystems = {
         "/efi" = {
           device = "/dev/disk/by-label/EFISYS";
           fsType = "vfat";
diff --git a/nixos/tests/hedgedoc.nix b/nixos/tests/hedgedoc.nix
new file mode 100644
index 0000000000000..657d49c555e99
--- /dev/null
+++ b/nixos/tests/hedgedoc.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+{
+  name = "hedgedoc";
+
+  meta = with lib.maintainers; {
+    maintainers = [ willibutz ];
+  };
+
+  nodes = {
+    hedgedocSqlite = { ... }: {
+      services = {
+        hedgedoc = {
+          enable = true;
+          configuration.dbURL = "sqlite:///var/lib/hedgedoc/hedgedoc.db";
+        };
+      };
+    };
+
+    hedgedocPostgres = { ... }: {
+      systemd.services.hedgedoc.after = [ "postgresql.service" ];
+      services = {
+        hedgedoc = {
+          enable = true;
+          configuration.dbURL = "postgres://hedgedoc:\${DB_PASSWORD}@localhost:5432/hedgedocdb";
+
+          /*
+           * Do not use pkgs.writeText for secrets as
+           * they will end up in the world-readable Nix store.
+           */
+          environmentFile = pkgs.writeText "hedgedoc-env" ''
+            DB_PASSWORD=snakeoilpassword
+          '';
+        };
+        postgresql = {
+          enable = true;
+          initialScript = pkgs.writeText "pg-init-script.sql" ''
+            CREATE ROLE hedgedoc LOGIN PASSWORD 'snakeoilpassword';
+            CREATE DATABASE hedgedocdb OWNER hedgedoc;
+          '';
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("HedgeDoc sqlite"):
+        hedgedocSqlite.wait_for_unit("hedgedoc.service")
+        hedgedocSqlite.wait_for_open_port(3000)
+        hedgedocSqlite.wait_until_succeeds("curl -sSf http://localhost:3000/new")
+
+    with subtest("HedgeDoc postgres"):
+        hedgedocPostgres.wait_for_unit("postgresql.service")
+        hedgedocPostgres.wait_for_unit("hedgedoc.service")
+        hedgedocPostgres.wait_for_open_port(5432)
+        hedgedocPostgres.wait_for_open_port(3000)
+        hedgedocPostgres.wait_until_succeeds("curl -sSf http://localhost:3000/new")
+  '';
+})
diff --git a/nixos/tests/herbstluftwm.nix b/nixos/tests/herbstluftwm.nix
new file mode 100644
index 0000000000000..2c98cceee6a21
--- /dev/null
+++ b/nixos/tests/herbstluftwm.nix
@@ -0,0 +1,38 @@
+import ./make-test-python.nix ({ lib, ...} : {
+  name = "herbstluftwm";
+
+  meta = {
+    maintainers = with lib.maintainers; [ thibautmarty ];
+    timeout = 30;
+  };
+
+  machine = { pkgs, lib, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = lib.mkForce "none+herbstluftwm";
+    services.xserver.windowManager.herbstluftwm.enable = true;
+    environment.systemPackages = [ pkgs.dzen2 ]; # needed for upstream provided panel
+  };
+
+  testScript = ''
+    with subtest("ensure x starts"):
+        machine.wait_for_x()
+        machine.wait_for_file("/home/alice/.Xauthority")
+        machine.succeed("xauth merge ~alice/.Xauthority")
+
+    with subtest("ensure client is available"):
+        machine.succeed("herbstclient --version")
+
+    with subtest("ensure keybindings are set"):
+        machine.wait_until_succeeds("herbstclient list_keybinds | grep xterm")
+
+    with subtest("ensure panel starts"):
+        machine.wait_for_window("dzen title")
+
+    with subtest("ensure we can open a new terminal"):
+        machine.send_key("alt-ret")
+        machine.wait_for_window(r"alice.*?machine")
+        machine.sleep(2)
+        machine.screenshot("terminal")
+  '';
+})
diff --git a/nixos/tests/hitch/default.nix b/nixos/tests/hitch/default.nix
index 904d12619d70e..a1d8e6162606d 100644
--- a/nixos/tests/hitch/default.nix
+++ b/nixos/tests/hitch/default.nix
@@ -1,7 +1,7 @@
 import ../make-test-python.nix ({ pkgs, ... }:
 {
   name = "hitch";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ jflanglois ];
   };
   machine = { pkgs, ... }: {
@@ -28,6 +28,6 @@ import ../make-test-python.nix ({ pkgs, ... }:
       machine.wait_for_unit("multi-user.target")
       machine.wait_for_unit("hitch.service")
       machine.wait_for_open_port(443)
-      assert "We are all good!" in machine.succeed("curl -k https://localhost:443/index.txt")
+      assert "We are all good!" in machine.succeed("curl -fk https://localhost:443/index.txt")
     '';
 })
diff --git a/nixos/tests/hledger-web.nix b/nixos/tests/hledger-web.nix
new file mode 100644
index 0000000000000..378d819437db0
--- /dev/null
+++ b/nixos/tests/hledger-web.nix
@@ -0,0 +1,53 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  journal = pkgs.writeText "test.journal" ''
+    2010/01/10 Loan
+        assets:cash                 500$
+        income:loan                -500$
+    2010/01/10 NixOS Foundation donation
+        expenses:donation           250$
+        assets:cash                -250$
+  '';
+in
+rec {
+  name = "hledger-web";
+  meta.maintainers = with lib.maintainers; [ marijanp ];
+
+  nodes = {
+    server = { config, pkgs, ... }: rec {
+      services.hledger-web = {
+        host = "127.0.0.1";
+        port = 5000;
+        enable = true;
+        journalFile = journal;
+      };
+      networking.firewall.allowedTCPPorts = [ services.hledger-web.port ];
+    };
+    apiserver = { config, pkgs, ... }: rec {
+      services.hledger-web = {
+        host = "127.0.0.1";
+        port = 5000;
+        enable = true;
+        serveApi = true;
+        journalFile = journal;
+      };
+      networking.firewall.allowedTCPPorts = [ services.hledger-web.port ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("hledger-web.service")
+    server.wait_for_open_port(5000)
+    with subtest("Check if web UI is accessible"):
+        page = server.succeed("curl -L http://127.0.0.1:5000")
+        assert "test.journal" in page
+
+    apiserver.wait_for_unit("hledger-web.service")
+    apiserver.wait_for_open_port(5000)
+    with subtest("Check if the JSON API is served"):
+        transactions = apiserver.succeed("curl -L http://127.0.0.1:5000/transactions")
+        assert "NixOS Foundation donation" in transactions
+  '';
+})
diff --git a/nixos/tests/hocker-fetchdocker/default.nix b/nixos/tests/hocker-fetchdocker/default.nix
index 978dbf310b122..e3979db3c60b5 100644
--- a/nixos/tests/hocker-fetchdocker/default.nix
+++ b/nixos/tests/hocker-fetchdocker/default.nix
@@ -1,6 +1,6 @@
 import ../make-test-python.nix ({ pkgs, ...} : {
   name = "test-hocker-fetchdocker";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ixmatus ];
     broken = true; # tries to download from registry-1.docker.io - how did this ever work?
   };
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index a93a28d877a31..131f50747fef0 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -6,7 +6,7 @@ let
   mqttPassword = "secret";
 in {
   name = "home-assistant";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ dotlambda ];
   };
 
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
index 3b87303d73e74..2e92b4259a6ab 100644
--- a/nixos/tests/hostname.nix
+++ b/nixos/tests/hostname.nix
@@ -7,13 +7,16 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
-  makeHostNameTest = hostName: domain:
+  makeHostNameTest = hostName: domain: fqdnOrNull:
     let
       fqdn = hostName + (optionalString (domain != null) ".${domain}");
+      getStr = str: # maybeString2String
+        let res = builtins.tryEval str;
+        in if (res.success && res.value != null) then res.value else "null";
     in
       makeTest {
         name = "hostname-${fqdn}";
-        meta = with pkgs.stdenv.lib.maintainers; {
+        meta = with pkgs.lib.maintainers; {
           maintainers = [ primeos blitz ];
         };
 
@@ -26,13 +29,16 @@ let
           ];
         };
 
-        testScript = ''
+        testScript = { nodes, ... }: ''
           start_all()
 
           machine = ${hostName}
 
           machine.wait_for_unit("network-online.target")
 
+          # Test if NixOS computes the correct FQDN (either a FQDN or an error/null):
+          assert "${getStr nodes.machine.config.networking.fqdn}" == "${getStr fqdnOrNull}"
+
           # 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()
@@ -60,7 +66,7 @@ let
 
 in
 {
-  noExplicitDomain = makeHostNameTest "ahost" null;
+  noExplicitDomain = makeHostNameTest "ahost" null null;
 
-  explicitDomain = makeHostNameTest "ahost" "adomain";
+  explicitDomain = makeHostNameTest "ahost" "adomain" "ahost.adomain";
 }
diff --git a/nixos/tests/hound.nix b/nixos/tests/hound.nix
index 27c65abdf27c3..4f51db1de9dec 100644
--- a/nixos/tests/hound.nix
+++ b/nixos/tests/hound.nix
@@ -1,7 +1,7 @@
 # Test whether `houndd` indexes nixpkgs
 import ./make-test-python.nix ({ pkgs, ... } : {
   name = "hound";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ grahamc ];
   };
   machine = { pkgs, ... }: {
@@ -53,7 +53,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     machine.wait_for_unit("hound.service")
     machine.wait_for_open_port(6080)
     machine.wait_until_succeeds(
-        "curl http://127.0.0.1:6080/api/v1/search\?stats\=fosho\&repos\=\*\&rng=%3A20\&q\=hi\&files\=\&i=nope | grep 'Filename' | grep 'hello'"
+        "curl -f http://127.0.0.1:6080/api/v1/search\?stats\=fosho\&repos\=\*\&rng=%3A20\&q\=hi\&files\=\&i=nope | grep 'Filename' | grep 'hello'"
     )
   '';
 })
diff --git a/nixos/tests/hydra/common.nix b/nixos/tests/hydra/common.nix
index 312c52e889a94..1a3a4d8fb3d4f 100644
--- a/nixos/tests/hydra/common.nix
+++ b/nixos/tests/hydra/common.nix
@@ -19,7 +19,7 @@
       buildInputs = [ pkgs.makeWrapper ];
       installPhase = "install -m755 -D ${./create-trivial-project.sh} $out/bin/create-trivial-project.sh";
       postFixup = ''
-        wrapProgram "$out/bin/create-trivial-project.sh" --prefix PATH ":" ${pkgs.stdenv.lib.makeBinPath [ pkgs.curl ]} --set EXPR_PATH ${trivialJob}
+        wrapProgram "$out/bin/create-trivial-project.sh" --prefix PATH ":" ${pkgs.lib.makeBinPath [ pkgs.curl ]} --set EXPR_PATH ${trivialJob}
       '';
     };
   in {
diff --git a/nixos/tests/hydra/db-migration.nix b/nixos/tests/hydra/db-migration.nix
deleted file mode 100644
index ca65e2e66aa1a..0000000000000
--- a/nixos/tests/hydra/db-migration.nix
+++ /dev/null
@@ -1,92 +0,0 @@
-{ system ? builtins.currentSystem
-, pkgs ? import ../../.. { inherit system; }
-, ...
-}:
-
-let inherit (import ./common.nix { inherit system; }) baseConfig; in
-
-with import ../../lib/testing-python.nix { inherit system pkgs; };
-with pkgs.lib;
-
-{ mig = makeTest {
-    name = "hydra-db-migration";
-    meta = with pkgs.stdenv.lib.maintainers; {
-      maintainers = [ ma27 ];
-    };
-
-    nodes = {
-      original = { pkgs, lib, ... }: {
-        imports = [ baseConfig ];
-
-        # An older version of Hydra before the db change
-        # for testing purposes.
-        services.hydra.package = pkgs.hydra-migration.overrideAttrs (old: {
-          inherit (old) pname;
-          version = "2020-02-06";
-          src = pkgs.fetchFromGitHub {
-            owner = "NixOS";
-            repo = "hydra";
-            rev = "2b4f14963b16b21ebfcd6b6bfa7832842e9b2afc";
-            sha256 = "16q0cffcsfx5pqd91n9k19850c1nbh4vvbd9h8yi64ihn7v8bick";
-          };
-        });
-      };
-
-      migration_phase1 = { pkgs, lib, ... }: {
-        imports = [ baseConfig ];
-        services.hydra.package = pkgs.hydra-migration;
-      };
-
-      finished = { pkgs, lib, ... }: {
-        imports = [ baseConfig ];
-        services.hydra.package = pkgs.hydra-unstable;
-      };
-    };
-
-    testScript = { nodes, ... }: let
-      next = nodes.migration_phase1.config.system.build.toplevel;
-      finished = nodes.finished.config.system.build.toplevel;
-    in ''
-      original.start()
-      original.wait_for_unit("multi-user.target")
-      original.wait_for_unit("postgresql.service")
-      original.wait_for_unit("hydra-init.service")
-      original.require_unit_state("hydra-queue-runner.service")
-      original.require_unit_state("hydra-evaluator.service")
-      original.require_unit_state("hydra-notify.service")
-      original.succeed("hydra-create-user admin --role admin --password admin")
-      original.wait_for_open_port(3000)
-      original.succeed("create-trivial-project.sh")
-      original.wait_until_succeeds(
-          '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+ builds\" -A'")
-      assert "jobset_id" not in out
-
-      original.succeed(
-          "${next}/bin/switch-to-configuration test >&2"
-      )
-      original.wait_for_unit("hydra-init.service")
-
-      out = original.succeed("su -l postgres -c 'psql -d hydra <<< \"\\d+ builds\" -A'")
-      assert "jobset_id|integer|||" in out
-
-      original.succeed("hydra-backfill-ids")
-
-      original.succeed(
-          "${finished}/bin/switch-to-configuration test >&2"
-      )
-      original.wait_for_unit("hydra-init.service")
-
-      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(
-          'curl -L -s http://localhost:3000/build/1 -H "Accept: application/json" |  jq .buildstatus | xargs test 0 -eq'
-      )
-
-      original.shutdown()
-    '';
-  };
-}
diff --git a/nixos/tests/hydra/default.nix b/nixos/tests/hydra/default.nix
index 2336e4033d6dc..d92f032b82922 100644
--- a/nixos/tests/hydra/default.nix
+++ b/nixos/tests/hydra/default.nix
@@ -11,12 +11,12 @@ let
   inherit (import ./common.nix { inherit system; }) baseConfig;
 
   hydraPkgs = {
-    inherit (pkgs) hydra-migration hydra-unstable;
+    inherit (pkgs) hydra-unstable;
   };
 
   makeHydraTest = with pkgs.lib; name: package: makeTest {
     name = "hydra-${name}";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ pstn lewo ma27 ];
     };
 
diff --git a/nixos/tests/i3wm.nix b/nixos/tests/i3wm.nix
index b527aa706ad21..59b4ffe3986e2 100644
--- a/nixos/tests/i3wm.nix
+++ b/nixos/tests/i3wm.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "i3wm";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aszlig ];
   };
 
diff --git a/nixos/tests/icingaweb2.nix b/nixos/tests/icingaweb2.nix
index 2f65604539c11..e631e667bd504 100644
--- a/nixos/tests/icingaweb2.nix
+++ b/nixos/tests/icingaweb2.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "icingaweb2";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ das_j ];
   };
 
diff --git a/nixos/tests/iftop.nix b/nixos/tests/iftop.nix
index 8a161027c2ad7..6d0090b39463d 100644
--- a/nixos/tests/iftop.nix
+++ b/nixos/tests/iftop.nix
@@ -4,7 +4,7 @@ with lib;
 
 {
   name = "iftop";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ ma27 ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
 
   nodes = {
     withIftop = {
diff --git a/nixos/tests/image-contents.nix b/nixos/tests/image-contents.nix
new file mode 100644
index 0000000000000..90908968a7e27
--- /dev/null
+++ b/nixos/tests/image-contents.nix
@@ -0,0 +1,51 @@
+# Tests the contents attribute of nixos/lib/make-disk-image.nix
+# including its user, group, and mode attributes.
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+with import common/ec2.nix { inherit makeTest pkgs; };
+
+let
+  config = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        fileSystems."/".device = "/dev/disk/by-label/nixos";
+        boot.loader.grub.device = "/dev/vda";
+        boot.loader.timeout = 0;
+      }
+    ];
+  }).config;
+  image = (import ../lib/make-disk-image.nix {
+    inherit pkgs config;
+    lib = pkgs.lib;
+    format = "qcow2";
+    contents = [{
+      source = pkgs.writeText "testFile" "contents";
+      target = "/testFile";
+      user = "1234";
+      group = "5678";
+      mode = "755";
+    }];
+  }) + "/nixos.qcow2";
+
+in makeEc2Test {
+  name = "image-contents";
+  inherit image;
+  userData = null;
+  script = ''
+    machine.start()
+    assert "content" in machine.succeed("cat /testFile")
+    fileDetails = machine.succeed("ls -l /testFile")
+    assert "1234" in fileDetails
+    assert "5678" in fileDetails
+    assert "rwxr-xr-x" in fileDetails
+  '';
+}
diff --git a/nixos/tests/influxdb.nix b/nixos/tests/influxdb.nix
index 04ef80461010b..03026f8404be5 100644
--- a/nixos/tests/influxdb.nix
+++ b/nixos/tests/influxdb.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "influxdb";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ offline ];
   };
 
diff --git a/nixos/tests/initrd-network-ssh/default.nix b/nixos/tests/initrd-network-ssh/default.nix
index 017de6882081d..0ad0563b0ce15 100644
--- a/nixos/tests/initrd-network-ssh/default.nix
+++ b/nixos/tests/initrd-network-ssh/default.nix
@@ -22,6 +22,10 @@ import ../make-test-python.nix ({ lib, ... }:
             hostKeys = [ ./ssh_host_ed25519_key ];
           };
         };
+        boot.initrd.extraUtilsCommands = ''
+          mkdir -p $out/secrets/etc/ssh
+          cat "${./ssh_host_ed25519_key}" > $out/secrets/etc/ssh/sh_host_ed25519_key
+        '';
         boot.initrd.preLVMCommands = ''
           while true; do
             if [ -f fnord ]; then
diff --git a/nixos/tests/initrd-network.nix b/nixos/tests/initrd-network.nix
index 9c35b7305768f..14e7e7d40bc59 100644
--- a/nixos/tests/initrd-network.nix
+++ b/nixos/tests/initrd-network.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "initrd-network";
 
-  meta.maintainers = [ pkgs.stdenv.lib.maintainers.eelco ];
+  meta.maintainers = [ pkgs.lib.maintainers.eelco ];
 
   machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
diff --git a/nixos/tests/initrd-secrets.nix b/nixos/tests/initrd-secrets.nix
new file mode 100644
index 0000000000000..10dd908502d5b
--- /dev/null
+++ b/nixos/tests/initrd-secrets.nix
@@ -0,0 +1,35 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, lib ? pkgs.lib
+, testing ? import ../lib/testing-python.nix { inherit system pkgs; }
+}:
+let
+  secretInStore = pkgs.writeText "topsecret" "iamasecret";
+  testWithCompressor = compressor: testing.makeTest {
+    name = "initrd-secrets-${compressor}";
+
+    meta.maintainers = [ lib.maintainers.lheckemann ];
+
+    machine = { ... }: {
+      virtualisation.useBootLoader = true;
+      boot.initrd.secrets."/test" = secretInStore;
+      boot.initrd.postMountCommands = ''
+        cp /test /mnt-root/secret-from-initramfs
+      '';
+      boot.initrd.compressor = compressor;
+      # zstd compression is only supported from 5.9 onwards. Remove when 5.10 becomes default.
+      boot.kernelPackages = pkgs.linuxPackages_latest;
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed(
+          "cmp ${secretInStore} /secret-from-initramfs"
+      )
+    '';
+  };
+in lib.flip lib.genAttrs testWithCompressor [
+  "cat" "gzip" "bzip2" "xz" "lzma" "lzop" "pigz" "pixz" "zstd"
+]
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
index 889a00d4b5686..e5d7009bb7b9a 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -94,6 +94,7 @@ in
   glib-networking = callInstalledTest ./glib-networking.nix {};
   gnome-photos = callInstalledTest ./gnome-photos.nix {};
   graphene = callInstalledTest ./graphene.nix {};
+  gsconnect = callInstalledTest ./gsconnect.nix {};
   ibus = callInstalledTest ./ibus.nix {};
   libgdata = callInstalledTest ./libgdata.nix {};
   glib-testing = callInstalledTest ./glib-testing.nix {};
@@ -101,5 +102,6 @@ in
   libxmlb = callInstalledTest ./libxmlb.nix {};
   malcontent = callInstalledTest ./malcontent.nix {};
   ostree = callInstalledTest ./ostree.nix {};
+  pipewire = callInstalledTest ./pipewire.nix {};
   xdg-desktop-portal = callInstalledTest ./xdg-desktop-portal.nix {};
 }
diff --git a/nixos/tests/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
index 6a0ceb57dda48..a8a683a1af7b0 100644
--- a/nixos/tests/installed-tests/fwupd.nix
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -5,7 +5,7 @@ makeInstalledTest {
 
   testConfig = {
     services.fwupd.enable = true;
-    services.fwupd.blacklistPlugins = lib.mkForce []; # don't blacklist test plugin
+    services.fwupd.disabledPlugins = lib.mkForce []; # don't disable test plugin
     services.fwupd.enableTestRemote = true;
     virtualisation.memorySize = 768;
   };
diff --git a/nixos/tests/installed-tests/gsconnect.nix b/nixos/tests/installed-tests/gsconnect.nix
new file mode 100644
index 0000000000000..ac39f7435786b
--- /dev/null
+++ b/nixos/tests/installed-tests/gsconnect.nix
@@ -0,0 +1,7 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gnomeExtensions.gsconnect;
+
+  withX11 = true;
+}
diff --git a/nixos/tests/installed-tests/pipewire.nix b/nixos/tests/installed-tests/pipewire.nix
new file mode 100644
index 0000000000000..f4154b5d2fd7d
--- /dev/null
+++ b/nixos/tests/installed-tests/pipewire.nix
@@ -0,0 +1,5 @@
+{ pkgs, lib, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.pipewire;
+}
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index d80cfb4bd83f2..904ec17229e1c 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -76,8 +76,8 @@ let
       def assemble_qemu_flags():
           flags = "-cpu max"
           ${if system == "x86_64-linux"
-            then ''flags += " -m 768"''
-            else ''flags += " -m 512 -enable-kvm -machine virt,gic-version=host"''
+            then ''flags += " -m 1024"''
+            else ''flags += " -m 768 -enable-kvm -machine virt,gic-version=host"''
           }
           return flags
 
@@ -270,7 +270,7 @@ let
     makeTest {
       inherit enableOCR;
       name = "installer-" + name;
-      meta = with pkgs.stdenv.lib.maintainers; {
+      meta = with pkgs.lib.maintainers; {
         # put global maintainers here, individuals go into makeInstallerTest fkt call
         maintainers = (meta.maintainers or []);
       };
@@ -284,7 +284,9 @@ let
             extraInstallerConfig
           ];
 
+          # builds stuff in the VM, needs more juice
           virtualisation.diskSize = 8 * 1024;
+          virtualisation.cores = 8;
           virtualisation.memorySize = 1536;
 
           # Use a small /dev/vdb as the root disk for the
@@ -323,10 +325,13 @@ let
             curl
           ]
           ++ optional (bootLoader == "grub" && grubVersion == 1) pkgs.grub
-          ++ optionals (bootLoader == "grub" && grubVersion == 2) [
-            pkgs.grub2
-            pkgs.grub2_efi
-          ];
+          ++ optionals (bootLoader == "grub" && grubVersion == 2) (let
+            zfsSupport = lib.any (x: x == "zfs")
+              (extraInstallerConfig.boot.supportedFilesystems or []);
+          in [
+            (pkgs.grub2.override { inherit zfsSupport; })
+            (pkgs.grub2_efi.override { inherit zfsSupport; })
+          ]);
 
           nix.binaryCaches = mkForce [ ];
           nix.extraOptions = ''
@@ -396,9 +401,9 @@ let
     createPartitions = ''
       machine.succeed(
           "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
-          + " mkpart ESP fat32 1M 50MiB"  # /boot
+          + " mkpart ESP fat32 1M 100MiB"  # /boot
           + " set 1 boot on"
-          + " mkpart primary linux-swap 50MiB 1024MiB"
+          + " mkpart primary linux-swap 100MiB 1024MiB"
           + " mkpart primary ext2 1024MiB -1MiB",  # /
           "udevadm settle",
           "mkswap /dev/vda2 -L swap",
diff --git a/nixos/tests/ipfs.nix b/nixos/tests/ipfs.nix
index 9c0ff5306e06a..f8683b0a85809 100644
--- a/nixos/tests/ipfs.nix
+++ b/nixos/tests/ipfs.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "ipfs";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mguentner ];
   };
 
diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix
index ba464b57447bb..f9d6d82b54acf 100644
--- a/nixos/tests/ipv6.nix
+++ b/nixos/tests/ipv6.nix
@@ -3,7 +3,7 @@
 
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "ipv6";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/jenkins.nix b/nixos/tests/jenkins.nix
index cd64ff5128788..5898adab759a1 100644
--- a/nixos/tests/jenkins.nix
+++ b/nixos/tests/jenkins.nix
@@ -5,7 +5,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "jenkins";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bjornfor coconnor domenkozar eelco ];
   };
 
diff --git a/nixos/tests/jitsi-meet.nix b/nixos/tests/jitsi-meet.nix
index 42762dfdad8e0..dec49c83121b9 100644
--- a/nixos/tests/jitsi-meet.nix
+++ b/nixos/tests/jitsi-meet.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "jitsi-meet";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = teams.jitsi.members;
   };
 
diff --git a/nixos/tests/jq.nix b/nixos/tests/jq.nix
new file mode 100644
index 0000000000000..075e6c43c09d4
--- /dev/null
+++ b/nixos/tests/jq.nix
@@ -0,0 +1,10 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "jq";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.jq = { pkgs, ... }: { environment.systemPackages = [ pkgs.jq ]; };
+
+  testScript = ''
+    assert "world" in jq.succeed('echo \'{"values":["hello","world"]}\'| jq \'.values[1]\''')
+  '';
+})
diff --git a/nixos/tests/k3s.nix b/nixos/tests/k3s.nix
index 5bda6f493f0e3..494a3b68b59d2 100644
--- a/nixos/tests/k3s.nix
+++ b/nixos/tests/k3s.nix
@@ -31,7 +31,7 @@ let
 in
 {
   name = "k3s";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ euank ];
   };
 
diff --git a/nixos/tests/kafka.nix b/nixos/tests/kafka.nix
index 88e30b62baa2d..d5c54f7d99107 100644
--- a/nixos/tests/kafka.nix
+++ b/nixos/tests/kafka.nix
@@ -8,7 +8,7 @@ with pkgs.lib;
 let
   makeKafkaTest = name: kafkaPackage: (import ./make-test-python.nix ({
     inherit name;
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ nequissimus ];
     };
 
@@ -80,15 +80,6 @@ let
   }) { inherit system; });
 
 in with pkgs; {
-  kafka_0_9  = makeKafkaTest "kafka_0_9"  apacheKafka_0_9;
-  kafka_0_10 = makeKafkaTest "kafka_0_10" apacheKafka_0_10;
-  kafka_0_11 = makeKafkaTest "kafka_0_11" apacheKafka_0_11;
-  kafka_1_0  = makeKafkaTest "kafka_1_0"  apacheKafka_1_0;
-  kafka_1_1  = makeKafkaTest "kafka_1_1"  apacheKafka_1_1;
-  kafka_2_0  = makeKafkaTest "kafka_2_0"  apacheKafka_2_0;
-  kafka_2_1  = makeKafkaTest "kafka_2_1"  apacheKafka_2_1;
-  kafka_2_2  = makeKafkaTest "kafka_2_2"  apacheKafka_2_2;
-  kafka_2_3  = makeKafkaTest "kafka_2_3"  apacheKafka_2_3;
   kafka_2_4  = makeKafkaTest "kafka_2_4"  apacheKafka_2_4;
   kafka_2_5  = makeKafkaTest "kafka_2_5"  apacheKafka_2_5;
 }
diff --git a/nixos/tests/keepassxc.nix b/nixos/tests/keepassxc.nix
new file mode 100644
index 0000000000000..98902187f6ac3
--- /dev/null
+++ b/nixos/tests/keepassxc.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "keepassxc";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ turion ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = [ pkgs.keepassxc ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    start_all()
+    machine.wait_for_x()
+
+    # start KeePassXC window
+    machine.execute("su - alice -c keepassxc &")
+
+    machine.wait_for_text("KeePassXC ${pkgs.keepassxc.version}")
+    machine.screenshot("KeePassXC")
+  '';
+})
diff --git a/nixos/tests/kernel-latest-ath-user-regd.nix b/nixos/tests/kernel-latest-ath-user-regd.nix
new file mode 100644
index 0000000000000..11a3959e692e9
--- /dev/null
+++ b/nixos/tests/kernel-latest-ath-user-regd.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "kernel-latest-ath-user-regd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ veehaitch ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      boot.kernelPackages = pkgs.linuxPackages_latest;
+      networking.wireless.athUserRegulatoryDomain = true;
+    };
+
+  testScript =
+    ''
+      assert "CONFIG_ATH_USER_REGD=y" in machine.succeed("zcat /proc/config.gz")
+    '';
+})
diff --git a/nixos/tests/kernel-latest.nix b/nixos/tests/kernel-latest.nix
index f09d0926d2236..323dde267a426 100644
--- a/nixos/tests/kernel-latest.nix
+++ b/nixos/tests/kernel-latest.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "kernel-latest";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
diff --git a/nixos/tests/kernel-lts.nix b/nixos/tests/kernel-lts.nix
index bad706d63c03a..9b03e9db6d84e 100644
--- a/nixos/tests/kernel-lts.nix
+++ b/nixos/tests/kernel-lts.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "kernel-lts";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
diff --git a/nixos/tests/kernel-testing.nix b/nixos/tests/kernel-testing.nix
index b7e10ebd5bd1c..017007c0aec85 100644
--- a/nixos/tests/kernel-testing.nix
+++ b/nixos/tests/kernel-testing.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "kernel-testing";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
new file mode 100644
index 0000000000000..45d8677af5675
--- /dev/null
+++ b/nixos/tests/keycloak.nix
@@ -0,0 +1,144 @@
+# This tests Keycloak: it starts the service, creates a realm with an
+# OIDC client and a user, and simulates the user logging in to the
+# client using their Keycloak login.
+
+let
+  frontendUrl = "http://keycloak/auth";
+  initialAdminPassword = "h4IhoJFnt2iQIR9";
+
+  keycloakTest = import ./make-test-python.nix (
+    { pkgs, databaseType, ... }:
+    {
+      name = "keycloak";
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ talyz ];
+      };
+
+      nodes = {
+        keycloak = { ... }: {
+          virtualisation.memorySize = 1024;
+          services.keycloak = {
+            enable = true;
+            inherit frontendUrl databaseType initialAdminPassword;
+            databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
+          };
+          environment.systemPackages = with pkgs; [
+            xmlstarlet
+            libtidy
+            jq
+          ];
+        };
+      };
+
+      testScript =
+        let
+          client = {
+            clientId = "test-client";
+            name = "test-client";
+            redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
+          };
+
+          user = {
+            firstName = "Chuck";
+            lastName = "Testa";
+            username = "chuck.testa";
+            email = "chuck.testa@example.com";
+          };
+
+          password = "password1234";
+
+          realm = {
+            enabled = true;
+            realm = "test-realm";
+            clients = [ client ];
+            users = [(
+              user // {
+                enabled = true;
+                credentials = [{
+                  type = "password";
+                  temporary = false;
+                  value = password;
+                }];
+              }
+            )];
+          };
+
+          realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
+
+          jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
+            if {
+              "firstName": .given_name,
+              "lastName": .family_name,
+              "username": .preferred_username,
+              "email": .email
+            } != ${builtins.toJSON user} then
+              error("Wrong user info!")
+            else
+              empty
+            end
+          '';
+        in ''
+          keycloak.start()
+          keycloak.wait_for_unit("keycloak.service")
+          keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
+
+
+          ### Realm Setup ###
+
+          # Get an admin interface access token
+          keycloak.succeed(
+              "curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >admin_auth_header"
+          )
+
+          # Publish the realm, including a test OIDC client and user
+          keycloak.succeed(
+              "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
+          )
+
+          # Generate and save the client secret. To do this we need
+          # Keycloak's internal id for the client.
+          keycloak.succeed(
+              "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
+              "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
+          )
+
+
+          ### Authentication Testing ###
+
+          # Start the login process by sending an initial request to the
+          # OIDC authentication endpoint, saving the returned page. Tidy
+          # up the HTML (XmlStarlet is picky) and extract the login form
+          # post url.
+          keycloak.succeed(
+              "curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form",
+              "tidy -q -m login_form || true",
+              "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']\" -v @action login_form >form_post_url",
+          )
+
+          # Post the login form and save the response. Once again tidy up
+          # the HTML, then extract the authorization code.
+          keycloak.succeed(
+              "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
+              "tidy -q -m auth_code_html || true",
+              "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
+          )
+
+          # Exchange the authorization code for an access token.
+          keycloak.succeed(
+              "curl -sSf -d grant_type=authorization_code -d code=$(<auth_code) -d client_id=${client.name} -d client_secret=$(<client_secret) -d redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >auth_header"
+          )
+
+          # Use the access token on the OIDC userinfo endpoint and check
+          # that the returned user info matches what we initialized the
+          # realm with.
+          keycloak.succeed(
+              "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
+          )
+        '';
+    }
+  );
+in
+{
+  postgres = keycloakTest { databaseType = "postgresql"; };
+  mysql = keycloakTest { databaseType = "mysql"; };
+}
diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix
index 8bab917a351e3..22279292f77f9 100644
--- a/nixos/tests/knot.nix
+++ b/nixos/tests/knot.nix
@@ -37,7 +37,7 @@ let
   '';
 in {
   name = "knot";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hexa ];
   };
 
diff --git a/nixos/tests/krb5/deprecated-config.nix b/nixos/tests/krb5/deprecated-config.nix
index be6ebce9e0516..9a9cafd4b13e9 100644
--- a/nixos/tests/krb5/deprecated-config.nix
+++ b/nixos/tests/krb5/deprecated-config.nix
@@ -3,7 +3,7 @@
 
 import ../make-test-python.nix ({ pkgs, ...} : {
   name = "krb5-with-deprecated-config";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eqyiel ];
   };
 
diff --git a/nixos/tests/krb5/example-config.nix b/nixos/tests/krb5/example-config.nix
index e2e10a9fda892..0932c71dd9705 100644
--- a/nixos/tests/krb5/example-config.nix
+++ b/nixos/tests/krb5/example-config.nix
@@ -3,7 +3,7 @@
 
 import ../make-test-python.nix ({ pkgs, ...} : {
   name = "krb5-with-example-config";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eqyiel ];
   };
 
diff --git a/nixos/tests/leaps.nix b/nixos/tests/leaps.nix
index ac0c602d44504..5cc387c86a452 100644
--- a/nixos/tests/leaps.nix
+++ b/nixos/tests/leaps.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs,  ... }:
 
 {
   name = "leaps";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ qknight ];
   };
 
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs,  ... }:
       server.wait_for_open_port(6666)
       client.wait_for_unit("network.target")
       assert "leaps" in client.succeed(
-          "${pkgs.curl}/bin/curl http://server:6666/leaps/"
+          "${pkgs.curl}/bin/curl -f http://server:6666/leaps/"
       )
     '';
 })
diff --git a/nixos/tests/lightdm.nix b/nixos/tests/lightdm.nix
index 46c2ed7ccc594..9611bdbdafec3 100644
--- a/nixos/tests/lightdm.nix
+++ b/nixos/tests/lightdm.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "lightdm";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aszlig worldofpeace ];
   };
 
diff --git a/nixos/tests/limesurvey.nix b/nixos/tests/limesurvey.nix
index 7228fcb833155..b60e80be2444c 100644
--- a/nixos/tests/limesurvey.nix
+++ b/nixos/tests/limesurvey.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "limesurvey";
-  meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
+  meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
   machine = { ... }: {
     services.limesurvey = {
@@ -20,7 +20,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
     machine.wait_for_unit("phpfpm-limesurvey.service")
     assert "The following surveys are available" in machine.succeed(
-        "curl http://example.local/"
+        "curl -f http://example.local/"
     )
   '';
 })
diff --git a/nixos/tests/locate.nix b/nixos/tests/locate.nix
new file mode 100644
index 0000000000000..e8ba41812a8f5
--- /dev/null
+++ b/nixos/tests/locate.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+  let inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+  in {
+    name = "locate";
+    meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
+
+    nodes = rec {
+      a = {
+        environment.systemPackages = with pkgs; [ sshfs ];
+        virtualisation.fileSystems = {
+          "/ssh" = {
+            device = "alice@b:/";
+            fsType = "fuse.sshfs";
+            options = [
+              "allow_other"
+              "IdentityFile=/privkey"
+              "noauto"
+              "StrictHostKeyChecking=no"
+              "UserKnownHostsFile=/dev/null"
+            ];
+          };
+        };
+        services.locate = {
+          enable = true;
+          interval = "*:*:0/5";
+        };
+      };
+      b = {
+        services.openssh.enable = true;
+        users.users.alice = {
+          isNormalUser = true;
+          openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      # Set up sshfs mount
+      a.succeed(
+          "(umask 077; cat ${snakeOilPrivateKey} > /privkey)"
+      )
+      b.succeed("touch /file-on-b-machine")
+      b.wait_for_open_port(22)
+      a.succeed("mkdir /ssh")
+      a.succeed("mount /ssh")
+
+      # Core locatedb functionality
+      a.succeed("touch /file-on-a-machine-1")
+      a.wait_for_file("/var/cache/locatedb")
+      a.wait_until_succeeds("locate file-on-a-machine-1")
+
+      # Wait for a second update to make sure we're using a locatedb from a run
+      # that began after the sshfs mount
+      a.succeed("touch /file-on-a-machine-2")
+      a.wait_until_succeeds("locate file-on-a-machine-2")
+
+      # We shouldn't be able to see files on the other machine
+      a.fail("locate file-on-b-machine")
+    '';
+  })
diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix
index d36c1a91be432..4d1dcc8cc32da 100644
--- a/nixos/tests/login.nix
+++ b/nixos/tests/login.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
 
 {
   name = "login";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
@@ -50,7 +50,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
       with subtest("Virtual console logout"):
           machine.send_chars("exit\n")
           machine.wait_until_fails("pgrep -u alice bash")
-          machine.screenshot("mingetty")
+          machine.screenshot("getty")
 
       with subtest("Check whether ctrl-alt-delete works"):
           machine.send_key("ctrl-alt-delete")
diff --git a/nixos/tests/loki.nix b/nixos/tests/loki.nix
index dbf1e8a650f5d..0c6dff3fdf137 100644
--- a/nixos/tests/loki.nix
+++ b/nixos/tests/loki.nix
@@ -12,15 +12,28 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
       enable = true;
       configFile = "${pkgs.grafana-loki.src}/cmd/loki/loki-local-config.yaml";
     };
-    systemd.services.promtail = {
-      description = "Promtail service for Loki test";
-      wantedBy = [ "multi-user.target" ];
-
-      serviceConfig = {
-        ExecStart = ''
-          ${pkgs.grafana-loki}/bin/promtail --config.file ${pkgs.grafana-loki.src}/cmd/promtail/promtail-local-config.yaml
-        '';
-        DynamicUser = true;
+    services.promtail = {
+      enable = true;
+      configuration = {
+        server = {
+          http_listen_port = 9080;
+          grpc_listen_port = 0;
+        };
+        clients = [ { url = "http://localhost:3100/loki/api/v1/push"; } ];
+        scrape_configs = [
+          {
+            job_name = "system";
+            static_configs = [
+              {
+                targets = [ "localhost" ];
+                labels = {
+                  job = "varlogs";
+                  __path__ = "/var/log/*log";
+                };
+              }
+            ];
+          }
+        ];
       };
     };
   };
@@ -32,6 +45,10 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
     machine.wait_for_open_port(3100)
     machine.wait_for_open_port(9080)
     machine.succeed("echo 'Loki Ingestion Test' > /var/log/testlog")
+    # should not have access to journal unless specified
+    machine.fail(
+        "systemctl show --property=SupplementaryGroups promtail | grep -q systemd-journal"
+    )
     machine.wait_until_succeeds(
         "${pkgs.grafana-loki}/bin/logcli --addr='http://localhost:3100' query --no-labels '{job=\"varlogs\",filename=\"/var/log/testlog\"}' | grep -q 'Loki Ingestion Test'"
     )
diff --git a/nixos/tests/lsd.nix b/nixos/tests/lsd.nix
new file mode 100644
index 0000000000000..c643f2f0b7b7d
--- /dev/null
+++ b/nixos/tests/lsd.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "lsd";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.lsd = { pkgs, ... }: { environment.systemPackages = [ pkgs.lsd ]; };
+
+  testScript = ''
+    lsd.succeed('echo "abc" > /tmp/foo')
+    assert "4 B /tmp/foo" in lsd.succeed('lsd --classic --blocks "size,name" -l /tmp/foo')
+    assert "lsd ${pkgs.lsd.version}" in lsd.succeed("lsd --version")
+  '';
+})
diff --git a/nixos/tests/lxd-nftables.nix b/nixos/tests/lxd-nftables.nix
index 4ca02067a0ae0..a62d5a3064dfb 100644
--- a/nixos/tests/lxd-nftables.nix
+++ b/nixos/tests/lxd-nftables.nix
@@ -8,7 +8,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "lxd-nftables";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ patryk27 ];
   };
 
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd.nix
index d1e642383cf86..ab56b75c02e4e 100644
--- a/nixos/tests/lxd.nix
+++ b/nixos/tests/lxd.nix
@@ -47,7 +47,7 @@ let
 in {
   name = "lxd";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ patryk27 ];
   };
 
diff --git a/nixos/tests/magic-wormhole-mailbox-server.nix b/nixos/tests/magic-wormhole-mailbox-server.nix
index 144a07e13492f..afdf7124fdc56 100644
--- a/nixos/tests/magic-wormhole-mailbox-server.nix
+++ b/nixos/tests/magic-wormhole-mailbox-server.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "magic-wormhole-mailbox-server";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
diff --git a/nixos/tests/magnetico.nix b/nixos/tests/magnetico.nix
index e79a728b2ac86..8433a974f453b 100644
--- a/nixos/tests/magnetico.nix
+++ b/nixos/tests/magnetico.nix
@@ -5,7 +5,7 @@ let
 in
 {
   name = "magnetico";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ rnhmjoj ];
   };
 
diff --git a/nixos/tests/mailcatcher.nix b/nixos/tests/mailcatcher.nix
index 2ef38544fe0a3..a55fba8a9950b 100644
--- a/nixos/tests/mailcatcher.nix
+++ b/nixos/tests/mailcatcher.nix
@@ -24,7 +24,7 @@ import ./make-test-python.nix ({ lib, ... }:
         'echo "this is the body of the email" | mail -s "subject" root@example.org'
     )
     assert "this is the body of the email" in machine.succeed(
-        "curl http://localhost:1080/messages/1.source"
+        "curl -f http://localhost:1080/messages/1.source"
     )
   '';
 })
diff --git a/nixos/tests/mailhog.nix b/nixos/tests/mailhog.nix
new file mode 100644
index 0000000000000..aece57178dd17
--- /dev/null
+++ b/nixos/tests/mailhog.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "mailhog";
+  meta.maintainers = with lib.maintainers; [ jojosch ];
+
+  machine = { pkgs, ... }: {
+    services.mailhog.enable = true;
+
+    environment.systemPackages = with pkgs; [ swaks ];
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("mailhog.service")
+    machine.wait_for_open_port("1025")
+    machine.wait_for_open_port("8025")
+    machine.succeed(
+        'echo "this is the body of the email" | swaks --to root@example.org --body - --server localhost:1025'
+    )
+    assert "this is the body of the email" in machine.succeed(
+        "curl --fail http://localhost:8025/api/v2/messages"
+    )
+  '';
+})
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix-synapse.nix
index 9ca808721763d..9a1ff8a0d3ed0 100644
--- a/nixos/tests/matrix-synapse.nix
+++ b/nixos/tests/matrix-synapse.nix
@@ -29,7 +29,7 @@ import ./make-test-python.nix ({ pkgs, ... } : let
 in {
 
   name = "matrix-synapse";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = teams.matrix.members;
   };
 
@@ -77,12 +77,12 @@ in {
     start_all()
     serverpostgres.wait_for_unit("matrix-synapse.service")
     serverpostgres.wait_until_succeeds(
-        "curl -L --cacert ${ca_pem} https://localhost:8448/"
+        "curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
     )
     serverpostgres.require_unit_state("postgresql.service")
     serversqlite.wait_for_unit("matrix-synapse.service")
     serversqlite.wait_until_succeeds(
-        "curl -L --cacert ${ca_pem} https://localhost:8448/"
+        "curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
     )
     serversqlite.succeed("[ -e /var/lib/matrix-synapse/homeserver.db ]")
   '';
diff --git a/nixos/tests/mediawiki.nix b/nixos/tests/mediawiki.nix
index 008682310cf65..702fefefa1610 100644
--- a/nixos/tests/mediawiki.nix
+++ b/nixos/tests/mediawiki.nix
@@ -22,7 +22,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
 
     machine.wait_for_unit("phpfpm-mediawiki.service")
 
-    page = machine.succeed("curl -L http://localhost/")
+    page = machine.succeed("curl -fL http://localhost/")
     assert "MediaWiki has been installed" in page
   '';
 })
diff --git a/nixos/tests/metabase.nix b/nixos/tests/metabase.nix
index 1450a4e9086f1..370114e922230 100644
--- a/nixos/tests/metabase.nix
+++ b/nixos/tests/metabase.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "metabase";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
@@ -15,6 +15,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     start_all()
     machine.wait_for_unit("metabase.service")
     machine.wait_for_open_port(3000)
-    machine.wait_until_succeeds("curl -L http://localhost:3000/setup | grep Metabase")
+    machine.wait_until_succeeds("curl -fL http://localhost:3000/setup | grep Metabase")
   '';
 })
diff --git a/nixos/tests/minecraft-server.nix b/nixos/tests/minecraft-server.nix
new file mode 100644
index 0000000000000..e6e0bca972a97
--- /dev/null
+++ b/nixos/tests/minecraft-server.nix
@@ -0,0 +1,37 @@
+let
+  seed = "2151901553968352745";
+  rcon-pass = "foobar";
+  rcon-port = 43000;
+in import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "minecraft-server";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.server = { ... }: {
+    environment.systemPackages = [ pkgs.mcrcon ];
+
+    nixpkgs.config.allowUnfree = true;
+
+    services.minecraft-server = {
+      declarative = true;
+      enable = true;
+      eula = true;
+      serverProperties = {
+        enable-rcon = true;
+        level-seed = seed;
+        online-mode = false;
+        "rcon.password" = rcon-pass;
+        "rcon.port" = rcon-port;
+      };
+    };
+
+    virtualisation.memorySize = 2048;
+  };
+
+  testScript = ''
+    server.wait_for_unit("minecraft-server")
+    server.wait_for_open_port(${toString rcon-port})
+    assert "${seed}" in server.succeed(
+        "mcrcon -H localhost -P ${toString rcon-port} -p '${rcon-pass}' -c 'seed'"
+    )
+  '';
+})
diff --git a/nixos/tests/minecraft.nix b/nixos/tests/minecraft.nix
new file mode 100644
index 0000000000000..3225ebac392ab
--- /dev/null
+++ b/nixos/tests/minecraft.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "minecraft";
+  meta = with lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.client = { nodes, ... }:
+      let user = nodes.client.config.users.users.alice;
+      in {
+        imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+        environment.systemPackages = [ pkgs.minecraft ];
+
+        nixpkgs.config.allowUnfree = true;
+
+        test-support.displayManager.auto.user = user.name;
+      };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }:
+    let user = nodes.client.config.users.users.alice;
+    in ''
+      client.wait_for_x()
+      client.execute("su - alice -c minecraft-launcher &")
+      client.wait_for_text("Create a new Microsoft account")
+      client.sleep(10)
+      client.screenshot("launcher")
+    '';
+})
diff --git a/nixos/tests/miniflux.nix b/nixos/tests/miniflux.nix
index 7d83d061a9df7..9f8b52c3c8570 100644
--- a/nixos/tests/miniflux.nix
+++ b/nixos/tests/miniflux.nix
@@ -11,7 +11,7 @@ in
 with lib;
 {
   name = "miniflux";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ bricewge ];
+  meta.maintainers = with pkgs.lib.maintainers; [ bricewge ];
 
   nodes = {
     default =
diff --git a/nixos/tests/minio.nix b/nixos/tests/minio.nix
index 02d1f7aa6c208..e49c517098aea 100644
--- a/nixos/tests/minio.nix
+++ b/nixos/tests/minio.nix
@@ -20,7 +20,7 @@ let
     '';
 in {
   name = "minio";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bachp ];
   };
 
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index ae15055327343..fb19b7060562f 100644
--- a/nixos/tests/misc.nix
+++ b/nixos/tests/misc.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : rec {
   name = "misc";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
@@ -16,7 +16,7 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
       environment.variables.EDITOR = mkOverride 0 "emacs";
       documentation.nixos.enable = mkOverride 0 true;
       systemd.tmpfiles.rules = [ "d /tmp 1777 root root 10d" ];
-      fileSystems = mkVMOverride { "/tmp2" =
+      virtualisation.fileSystems = { "/tmp2" =
         { fsType = "tmpfs";
           options = [ "mode=1777" "noauto" ];
         };
@@ -88,8 +88,8 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
       with subtest("whether kernel.poweroff_cmd is set"):
           machine.succeed('[ -x "$(cat /proc/sys/kernel/poweroff_cmd)" ]')
 
-      with subtest("whether the blkio controller is properly enabled"):
-          machine.succeed("[ -e /sys/fs/cgroup/blkio/blkio.reset_stats ]")
+      with subtest("whether the io cgroupv2 controller is properly enabled"):
+          machine.succeed("grep -q '\\bio\\b' /sys/fs/cgroup/cgroup.controllers")
 
       with subtest("whether we have a reboot record in wtmp"):
           machine.shutdown
diff --git a/nixos/tests/molly-brown.nix b/nixos/tests/molly-brown.nix
index 09ce42726ca98..bfc036e81ba04 100644
--- a/nixos/tests/molly-brown.nix
+++ b/nixos/tests/molly-brown.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   in {
 
     name = "molly-brown";
-    meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ ehmry ]; };
+    meta = with pkgs.lib.maintainers; { maintainers = [ ehmry ]; };
 
     nodes = {
 
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
index 1a71238830189..9c6fdfb1ca76e 100644
--- a/nixos/tests/mongodb.nix
+++ b/nixos/tests/mongodb.nix
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
   in {
     name = "mongodb";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ bluescreen303 offline cstrahan rvl phile314 ];
     };
 
diff --git a/nixos/tests/morty.nix b/nixos/tests/morty.nix
index 64c5a27665d6c..9909596820d31 100644
--- a/nixos/tests/morty.nix
+++ b/nixos/tests/morty.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "morty";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ leenaars ];
   };
 
@@ -12,9 +12,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
       { ... }:
       { services.morty = {
         enable = true;
-	key = "78a9cd0cfee20c672f78427efb2a2a96036027f0";
-	port = 3001;
-	};
+        key = "78a9cd0cfee20c672f78427efb2a2a96036027f0";
+        port = 3001;
+        };
       };
 
     };
@@ -24,7 +24,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     ''
       mortyProxyWithKey.wait_for_unit("default.target")
       mortyProxyWithKey.wait_for_open_port(3001)
-      mortyProxyWithKey.succeed("curl -L 127.0.0.1:3001 | grep MortyProxy")
+      mortyProxyWithKey.succeed("curl -fL 127.0.0.1:3001 | grep MortyProxy")
     '';
 
 })
diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix
index 1f2fdf4237fa7..308c1396013d6 100644
--- a/nixos/tests/mosquitto.nix
+++ b/nixos/tests/mosquitto.nix
@@ -7,7 +7,7 @@ let
   topic = "test/foo";
 in {
   name = "mosquitto";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ peterhoeg ];
   };
 
diff --git a/nixos/tests/mpd.nix b/nixos/tests/mpd.nix
index 60aef586ad5c9..5c969fc9c9176 100644
--- a/nixos/tests/mpd.nix
+++ b/nixos/tests/mpd.nix
@@ -27,10 +27,12 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       after = [ "mpd.service" ];
       wantedBy = [ "default.target" ];
       script = ''
-        mkdir -p ${musicDirectory} && chown -R ${user}:${group} ${musicDirectory}
         cp ${track} ${musicDirectory}
-        chown ${user}:${group} ${musicDirectory}/$(basename ${track})
       '';
+      serviceConfig = {
+        User = user;
+        Group = group;
+      };
     };
 
     mkServer = { mpd, musicService, }:
@@ -41,7 +43,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       };
   in {
     name = "mpd";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ emmanuelrosa ];
     };
 
@@ -105,7 +107,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
         for track in tracks.splitlines():
             server.succeed(f"{mpc} add {track}")
 
-        _, added_tracks = server.execute(f"{mpc} listall")
+        _, added_tracks = server.execute(f"{mpc} playlist")
 
         # Check we succeeded adding audio tracks to the playlist
         assert len(added_tracks.splitlines()) > 0
diff --git a/nixos/tests/mumble.nix b/nixos/tests/mumble.nix
index cb3e0ec42fc58..717f3c7892888 100644
--- a/nixos/tests/mumble.nix
+++ b/nixos/tests/mumble.nix
@@ -14,7 +14,7 @@ let
 in
 {
   name = "mumble";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ thoughtpolice eelco ];
   };
 
diff --git a/nixos/tests/munin.nix b/nixos/tests/munin.nix
index 7b674db7768d9..4ec17e0339df0 100644
--- a/nixos/tests/munin.nix
+++ b/nixos/tests/munin.nix
@@ -3,7 +3,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "munin";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ domenkozar eelco ];
   };
 
@@ -27,7 +27,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           };
 
           # increase the systemd timer interval so it fires more often
-          systemd.timers.munin-cron.timerConfig.OnCalendar = pkgs.stdenv.lib.mkForce "*:*:0/10";
+          systemd.timers.munin-cron.timerConfig.OnCalendar = pkgs.lib.mkForce "*:*:0/10";
         };
     };
 
diff --git a/nixos/tests/mutable-users.nix b/nixos/tests/mutable-users.nix
index 49c7f78b82edf..e3f002d9b1988 100644
--- a/nixos/tests/mutable-users.nix
+++ b/nixos/tests/mutable-users.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "mutable-users";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ gleber ];
   };
 
diff --git a/nixos/tests/mxisd.nix b/nixos/tests/mxisd.nix
index b2b60db4d8227..22755ea353b64 100644
--- a/nixos/tests/mxisd.nix
+++ b/nixos/tests/mxisd.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... } : {
 
   name = "mxisd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mguentner ];
   };
 
diff --git a/nixos/tests/mysql/mariadb-galera-mariabackup.nix b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
index cae55878060c7..a4b893a9f33a9 100644
--- a/nixos/tests/mysql/mariadb-galera-mariabackup.nix
+++ b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
@@ -6,7 +6,7 @@ let
 
 in {
   name = "mariadb-galera-mariabackup";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ izorkin ];
   };
 
diff --git a/nixos/tests/mysql/mariadb-galera-rsync.nix b/nixos/tests/mysql/mariadb-galera-rsync.nix
index 4318efae8a936..6fb3cfef8d731 100644
--- a/nixos/tests/mysql/mariadb-galera-rsync.nix
+++ b/nixos/tests/mysql/mariadb-galera-rsync.nix
@@ -6,7 +6,7 @@ let
 
 in {
   name = "mariadb-galera-rsync";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ izorkin ];
   };
 
diff --git a/nixos/tests/mysql/mysql-backup.nix b/nixos/tests/mysql/mysql-backup.nix
index c4c1079a8a641..d428fb6c16e68 100644
--- a/nixos/tests/mysql/mysql-backup.nix
+++ b/nixos/tests/mysql/mysql-backup.nix
@@ -1,7 +1,7 @@
 # Test whether mysqlBackup option works
 import ./../make-test-python.nix ({ pkgs, ... } : {
   name = "mysql-backup";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ rvl ];
   };
 
diff --git a/nixos/tests/mysql/mysql-replication.nix b/nixos/tests/mysql/mysql-replication.nix
index b5e0032501936..ad84c801ea106 100644
--- a/nixos/tests/mysql/mysql-replication.nix
+++ b/nixos/tests/mysql/mysql-replication.nix
@@ -7,7 +7,7 @@ in
 
 {
   name = "mysql-replication";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco shlevy ];
   };
 
diff --git a/nixos/tests/mysql/mysql.nix b/nixos/tests/mysql/mysql.nix
index 5437a2860437e..50ad5c68aef1d 100644
--- a/nixos/tests/mysql/mysql.nix
+++ b/nixos/tests/mysql/mysql.nix
@@ -1,6 +1,6 @@
 import ./../make-test-python.nix ({ pkgs, ...} : {
   name = "mysql";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco shlevy ];
   };
 
@@ -98,7 +98,7 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
         }];
         services.mysql.settings = {
           mysqld = {
-            plugin-load-add = [ "ha_tokudb.so" "ha_rocksdb.so" ];
+            plugin-load-add = [ "ha_rocksdb.so" ];
           };
         };
         services.mysql.package = pkgs.mariadb;
@@ -185,19 +185,5 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
     mariadb.succeed(
         "echo 'use testdb; drop table rocksdb;' | sudo -u testuser mysql -u testuser"
     )
-  '' + pkgs.stdenv.lib.optionalString pkgs.stdenv.isx86_64 ''
-    # Check if TokuDB 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"
-    )
-    mariadb.succeed(
-        "echo 'use testdb; insert into tokudb values (25);' | 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"
-    )
-    mariadb.succeed(
-        "echo 'use testdb; drop table tokudb;' | sudo -u testuser mysql -u testuser"
-    )
   '';
 })
diff --git a/nixos/tests/n8n.nix b/nixos/tests/n8n.nix
new file mode 100644
index 0000000000000..ed93639f2a429
--- /dev/null
+++ b/nixos/tests/n8n.nix
@@ -0,0 +1,25 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = 5678;
+in
+{
+  name = "n8n";
+  meta.maintainers = with maintainers; [ freezeboy ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      services.n8n = {
+        enable = true;
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("n8n.service")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("curl --fail http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/nagios.nix b/nixos/tests/nagios.nix
index 6f5d444728788..e4d8dabedf72c 100644
--- a/nixos/tests/nagios.nix
+++ b/nixos/tests/nagios.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix (
   { pkgs, ... }: {
     name = "nagios";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ symphorien ];
     };
 
diff --git a/nixos/tests/nano.nix b/nixos/tests/nano.nix
new file mode 100644
index 0000000000000..6585a6842e857
--- /dev/null
+++ b/nixos/tests/nano.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "nano";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { lib, ... }: {
+    environment.systemPackages = [ pkgs.nano ];
+  };
+
+  testScript = { ... }: ''
+    start_all()
+
+    with subtest("Create user and log in"):
+        machine.wait_for_unit("multi-user.target")
+        machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+        machine.succeed("useradd -m alice")
+        machine.succeed("(echo foobar; echo foobar) | passwd alice")
+        machine.wait_until_tty_matches(1, "login: ")
+        machine.send_chars("alice\n")
+        machine.wait_until_tty_matches(1, "login: alice")
+        machine.wait_until_succeeds("pgrep login")
+        machine.wait_until_tty_matches(1, "Password: ")
+        machine.send_chars("foobar\n")
+        machine.wait_until_succeeds("pgrep -u alice bash")
+        machine.screenshot("prompt")
+
+    with subtest("Use nano"):
+        machine.send_chars("nano /tmp/foo")
+        machine.send_key("ret")
+        machine.sleep(2)
+        machine.send_chars("42")
+        machine.sleep(1)
+        machine.send_key("ctrl-x")
+        machine.sleep(1)
+        machine.send_key("y")
+        machine.sleep(1)
+        machine.screenshot("nano")
+        machine.sleep(1)
+        machine.send_key("ret")
+        machine.wait_for_file("/tmp/foo")
+        assert "42" in machine.succeed("cat /tmp/foo")
+  '';
+})
diff --git a/nixos/tests/nar-serve.nix b/nixos/tests/nar-serve.nix
new file mode 100644
index 0000000000000..9ee738ffb170e
--- /dev/null
+++ b/nixos/tests/nar-serve.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "nar-serve";
+    meta.maintainers = [ lib.maintainers.rizary ];
+    nodes =
+      {
+        server = { pkgs, ... }: {
+          services.nginx = {
+            enable = true;
+            virtualHosts.default.root = "/var/www";
+          };
+          services.nar-serve = {
+            enable = true;
+            # Connect to the localhost nginx instead of the default
+            # https://cache.nixos.org
+            cacheURL = "http://localhost/";
+          };
+          environment.systemPackages = [
+            pkgs.hello
+            pkgs.curl
+          ];
+
+          networking.firewall.allowedTCPPorts = [ 8383 ];
+
+          # virtualisation.diskSize = 2 * 1024;
+        };
+      };
+    testScript = ''
+      start_all()
+
+      # Create a fake cache with Nginx service the static files
+      server.succeed(
+          "nix copy --to file:///var/www ${pkgs.hello}"
+      )
+      server.wait_for_unit("nginx.service")
+      server.wait_for_open_port(80)
+
+      # Check that nar-serve can return the content of the derivation
+      drvName = os.path.basename("${pkgs.hello}")
+      drvHash = drvName.split("-")[0]
+      server.wait_for_unit("nar-serve.service")
+      server.succeed(
+          "curl -o hello -f http://localhost:8383/nix/store/{}/bin/hello".format(drvHash)
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/nat.nix b/nixos/tests/nat.nix
index 0d1f7aaedfa23..545eb46f2bf59 100644
--- a/nixos/tests/nat.nix
+++ b/nixos/tests/nat.nix
@@ -23,7 +23,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
   {
     name = "nat" + (if withFirewall then "WithFirewall" else "Standalone")
                  + (lib.optionalString withConntrackHelpers "withConntrackHelpers");
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ eelco rob ];
     };
 
diff --git a/nixos/tests/ncdns.nix b/nixos/tests/ncdns.nix
index 9960ac63e26b0..50193676f34f3 100644
--- a/nixos/tests/ncdns.nix
+++ b/nixos/tests/ncdns.nix
@@ -24,7 +24,7 @@ in
 
 {
   name = "ncdns";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ rnhmjoj ];
   };
 
diff --git a/nixos/tests/ndppd.nix b/nixos/tests/ndppd.nix
index b67b26a79341f..e79e2a097b406 100644
--- a/nixos/tests/ndppd.nix
+++ b/nixos/tests/ndppd.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "ndppd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ fpletz ];
   };
 
diff --git a/nixos/tests/neo4j.nix b/nixos/tests/neo4j.nix
index 32ee7f501b8bc..8329e5630d7af 100644
--- a/nixos/tests/neo4j.nix
+++ b/nixos/tests/neo4j.nix
@@ -15,6 +15,6 @@ import ./make-test-python.nix {
 
     master.wait_for_unit("neo4j")
     master.wait_for_open_port(7474)
-    master.succeed("curl http://localhost:7474/")
+    master.succeed("curl -f http://localhost:7474/")
   '';
 }
diff --git a/nixos/tests/netdata.nix b/nixos/tests/netdata.nix
index 4ddc96e8bc224..0f26630da9d4b 100644
--- a/nixos/tests/netdata.nix
+++ b/nixos/tests/netdata.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "netdata";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ cransom ];
   };
 
diff --git a/nixos/tests/networking-proxy.nix b/nixos/tests/networking-proxy.nix
index bae9c66ed61a2..62b5e690f6d1e 100644
--- a/nixos/tests/networking-proxy.nix
+++ b/nixos/tests/networking-proxy.nix
@@ -12,7 +12,7 @@ let default-config = {
       };
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "networking-proxy";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [  ];
   };
 
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 83d4f6465b686..1ea61f99a9513 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -35,7 +35,7 @@ let
         extraConfig = flip concatMapStrings vlanIfs (n: ''
           subnet 192.168.${toString n}.0 netmask 255.255.255.0 {
             option routers 192.168.${toString n}.1;
-            range 192.168.${toString n}.2 192.168.${toString n}.254;
+            range 192.168.${toString n}.3 192.168.${toString n}.254;
           }
         '')
         ;
@@ -499,8 +499,8 @@ let
                 list, targetList
             )
         with subtest("Test MTU and MAC Address are configured"):
-            assert "mtu 1342" in machine.succeed("ip link show dev tap0")
-            assert "mtu 1343" in machine.succeed("ip link show dev tun0")
+            machine.wait_until_succeeds("ip link show dev tap0 | grep 'mtu 1342'")
+            machine.wait_until_succeeds("ip link show dev tun0 | grep 'mtu 1343'")
             assert "02:de:ad:be:ef:01" in machine.succeed("ip link show dev tap0")
       '' # network-addresses-* only exist in scripted networking
       + optionalString (!networkd) ''
@@ -672,6 +672,30 @@ let
             ), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue)
       '';
     };
+    rename = {
+      name = "RenameInterface";
+      machine = { pkgs, ... }: {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+        };
+      } //
+      (if networkd
+       then { systemd.network.links."10-custom_name" = {
+                matchConfig.MACAddress = "52:54:00:12:01:01";
+                linkConfig.Name = "custom_name";
+              };
+            }
+       else { services.udev.initrdRules = ''
+               SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="custom_name"
+              '';
+            });
+      testScript = ''
+        machine.succeed("udevadm settle")
+        print(machine.succeed("ip link show dev custom_name"))
+      '';
+    };
     # even with disabled networkd, systemd.network.links should work
     # (as it's handled by udev, not networkd)
     link = {
diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix
index 72fb020dca708..0b8e1937128c9 100644
--- a/nixos/tests/nextcloud/basic.nix
+++ b/nixos/tests/nextcloud/basic.nix
@@ -3,7 +3,7 @@ import ../make-test-python.nix ({ pkgs, ...}: let
   adminuser = "root";
 in {
   name = "nextcloud-basic";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ globin eqyiel ];
   };
 
@@ -15,7 +15,7 @@ in {
         echo "http://nextcloud/remote.php/webdav/ ${adminuser} ${adminpass}" > /tmp/davfs2-secrets
         chmod 600 /tmp/davfs2-secrets
       '';
-      fileSystems = pkgs.lib.mkVMOverride {
+      virtualisation.fileSystems = {
         "/mnt/dav" = {
           device = "http://nextcloud/remote.php/webdav/";
           fsType = "davfs";
@@ -42,6 +42,7 @@ in {
           enable = true;
           startAt = "20:00";
         };
+        phpExtraExtensions = all: [ all.bz2 ];
       };
 
       environment.systemPackages = [ cfg.services.nextcloud.occ ];
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
index bec3815a3e14c..82041874de43f 100644
--- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -3,7 +3,7 @@ import ../make-test-python.nix ({ pkgs, ...}: let
   adminuser = "root";
 in {
   name = "nextcloud-with-mysql-and-memcached";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eqyiel ];
   };
 
diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
index 40a208115c32c..81af620598ee8 100644
--- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix
+++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
@@ -3,7 +3,7 @@ import ../make-test-python.nix ({ pkgs, ...}: let
   adminuser = "custom-admin-username";
 in {
   name = "nextcloud-with-postgresql-and-redis";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eqyiel ];
   };
 
diff --git a/nixos/tests/nexus.nix b/nixos/tests/nexus.nix
index 1ec5c40476a68..2a30a4eb2cc87 100644
--- a/nixos/tests/nexus.nix
+++ b/nixos/tests/nexus.nix
@@ -5,7 +5,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "nexus";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ironpinguin ma27 ];
   };
 
diff --git a/nixos/tests/nfs/kerberos.nix b/nixos/tests/nfs/kerberos.nix
index 078f0b7814cea..75d1210496b00 100644
--- a/nixos/tests/nfs/kerberos.nix
+++ b/nixos/tests/nfs/kerberos.nix
@@ -40,7 +40,7 @@ in
         networking.domain = "nfs.test";
         networking.hostName = "client";
 
-        fileSystems = lib.mkVMOverride
+        virtualisation.fileSystems =
           { "/data" = {
               device  = "server.nfs.test:/";
               fsType  = "nfs";
diff --git a/nixos/tests/nfs/simple.nix b/nixos/tests/nfs/simple.nix
index c49ebddc2fddf..6a01089c0828c 100644
--- a/nixos/tests/nfs/simple.nix
+++ b/nixos/tests/nfs/simple.nix
@@ -4,7 +4,7 @@ let
 
   client =
     { pkgs, ... }:
-    { fileSystems = pkgs.lib.mkVMOverride
+    { virtualisation.fileSystems =
         { "/data" =
            { # nfs4 exports the export with fsid=0 as a virtual root directory
              device = if (version == 4) then "server:/" else "server:/data";
@@ -19,7 +19,7 @@ in
 
 {
   name = "nfs";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/nginx-auth.nix b/nixos/tests/nginx-auth.nix
new file mode 100644
index 0000000000000..c0d24a20ddbcc
--- /dev/null
+++ b/nixos/tests/nginx-auth.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx-auth";
+
+  nodes = {
+    webserver = { pkgs, lib, ... }: {
+      services.nginx = let
+        root = pkgs.runCommand "testdir" {} ''
+          mkdir "$out"
+          echo hello world > "$out/index.html"
+        '';
+      in {
+        enable = true;
+
+        virtualHosts.lockedroot = {
+          inherit root;
+          basicAuth.alice = "jane";
+        };
+
+        virtualHosts.lockedsubdir = {
+          inherit root;
+          locations."/sublocation/" = {
+            alias = "${root}/";
+            basicAuth.bob = "john";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    webserver.wait_for_unit("nginx")
+    webserver.wait_for_open_port(80)
+
+    webserver.fail("curl --fail --resolve lockedroot:80:127.0.0.1 http://lockedroot")
+    webserver.succeed(
+        "curl --fail --resolve lockedroot:80:127.0.0.1 http://alice:jane@lockedroot"
+    )
+
+    webserver.succeed("curl --fail --resolve lockedsubdir:80:127.0.0.1 http://lockedsubdir")
+    webserver.fail(
+        "curl --fail --resolve lockedsubdir:80:127.0.0.1 http://lockedsubdir/sublocation/index.html"
+    )
+    webserver.succeed(
+        "curl --fail --resolve lockedsubdir:80:127.0.0.1 http://bob:john@lockedsubdir/sublocation/index.html"
+    )
+  '';
+})
diff --git a/nixos/tests/nginx-sandbox.nix b/nixos/tests/nginx-sandbox.nix
index 514318c9456c4..2d512725f2650 100644
--- a/nixos/tests/nginx-sandbox.nix
+++ b/nixos/tests/nginx-sandbox.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "nginx-sandbox";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ izorkin ];
   };
 
diff --git a/nixos/tests/nginx-sso.nix b/nixos/tests/nginx-sso.nix
index 8834fc31c387b..aeb89859c73f1 100644
--- a/nixos/tests/nginx-sso.nix
+++ b/nixos/tests/nginx-sso.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "nginx-sso";
   meta = {
-    maintainers = with pkgs.stdenv.lib.maintainers; [ delroth ];
+    maintainers = with pkgs.lib.maintainers; [ delroth ];
   };
 
   machine = {
diff --git a/nixos/tests/nginx.nix b/nixos/tests/nginx.nix
index 18822f095688b..5686afcd043e8 100644
--- a/nixos/tests/nginx.nix
+++ b/nixos/tests/nginx.nix
@@ -6,7 +6,7 @@
 #   3. nginx doesn't restart on configuration changes (only reloads)
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "nginx";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mbbx6spp danbst ];
   };
 
diff --git a/nixos/tests/nixos-generate-config.nix b/nixos/tests/nixos-generate-config.nix
index 6c83ccecc70a0..7bf8d4da7b6d4 100644
--- a/nixos/tests/nixos-generate-config.nix
+++ b/nixos/tests/nixos-generate-config.nix
@@ -7,8 +7,15 @@ import ./make-test-python.nix ({ lib, ... } : {
       { config, pkgs, ... }: {
         imports = [ ./hardware-configuration.nix ];
       $bootLoaderConfig
+      $desktopConfiguration
       }
     '';
+
+    system.nixos-generate-config.desktopConfiguration = [''
+      # DESKTOP
+      services.xserver.displayManager.gdm.enable = true;
+      services.xserver.desktopManager.gnome3.enable = true;
+    ''];
   };
   testScript = ''
     start_all()
@@ -18,9 +25,17 @@ import ./make-test-python.nix ({ lib, ... } : {
     # Test if the configuration really is overridden
     machine.succeed("grep 'OVERRIDDEN' /etc/nixos/configuration.nix")
 
+    # Test if desktop configuration really is overridden
+    machine.succeed("grep 'DESKTOP' /etc/nixos/configuration.nix")
+
     # Test of if the Perl variable $bootLoaderConfig is spliced correctly:
     machine.succeed(
         "grep 'boot\\.loader\\.grub\\.enable = true;' /etc/nixos/configuration.nix"
     )
+
+    # Test if the Perl variable $desktopConfiguration is spliced correctly
+    machine.succeed(
+        "grep 'services\\.xserver\\.desktopManager\\.gnome3\\.enable = true;' /etc/nixos/configuration.nix"
+    )
   '';
 })
diff --git a/nixos/tests/nomad.nix b/nixos/tests/nomad.nix
new file mode 100644
index 0000000000000..51b11a8fef909
--- /dev/null
+++ b/nixos/tests/nomad.nix
@@ -0,0 +1,97 @@
+import ./make-test-python.nix (
+  { lib, ... }: {
+    name = "nomad";
+    nodes = {
+      default_server = { pkgs, lib, ... }: {
+        networking = {
+          interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [{
+            address = "192.168.1.1";
+            prefixLength = 16;
+          }];
+        };
+
+        environment.etc."nomad.custom.json".source =
+          (pkgs.formats.json { }).generate "nomad.custom.json" {
+            region = "universe";
+            datacenter = "earth";
+          };
+
+        services.nomad = {
+          enable = true;
+
+          settings = {
+            server = {
+              enabled = true;
+              bootstrap_expect = 1;
+            };
+          };
+
+          extraSettingsPaths = [ "/etc/nomad.custom.json" ];
+          enableDocker = false;
+        };
+      };
+
+      custom_state_dir_server = { pkgs, lib, ... }: {
+        networking = {
+          interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [{
+            address = "192.168.1.1";
+            prefixLength = 16;
+          }];
+        };
+
+        environment.etc."nomad.custom.json".source =
+          (pkgs.formats.json { }).generate "nomad.custom.json" {
+            region = "universe";
+            datacenter = "earth";
+          };
+
+        services.nomad = {
+          enable = true;
+          dropPrivileges = false;
+
+          settings = {
+            data_dir = "/nomad/data/dir";
+            server = {
+              enabled = true;
+              bootstrap_expect = 1;
+            };
+          };
+
+          extraSettingsPaths = [ "/etc/nomad.custom.json" ];
+          enableDocker = false;
+        };
+
+        systemd.services.nomad.serviceConfig.ExecStartPre = "${pkgs.writeShellScript "mk_data_dir" ''
+          set -euxo pipefail
+
+          ${pkgs.coreutils}/bin/mkdir -p /nomad/data/dir
+        ''}";
+      };
+    };
+
+    testScript = ''
+      def test_nomad_server(server):
+          server.wait_for_unit("nomad.service")
+
+          # wait for healthy server
+          server.wait_until_succeeds(
+              "[ $(nomad operator raft list-peers | grep true | wc -l) == 1 ]"
+          )
+
+          # wait for server liveness
+          server.succeed("[ $(nomad server members | grep -o alive | wc -l) == 1 ]")
+
+          # check the region
+          server.succeed("nomad server members | grep -o universe")
+
+          # check the datacenter
+          server.succeed("[ $(nomad server members | grep -o earth | wc -l) == 1 ]")
+
+
+      servers = [default_server, custom_state_dir_server]
+
+      for server in servers:
+          test_nomad_server(server)
+    '';
+  }
+)
diff --git a/nixos/tests/novacomd.nix b/nixos/tests/novacomd.nix
index 940210dee2355..b470c117e1e15 100644
--- a/nixos/tests/novacomd.nix
+++ b/nixos/tests/novacomd.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "novacomd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ dtzWill ];
   };
 
diff --git a/nixos/tests/nsd.nix b/nixos/tests/nsd.nix
index bcc14e817a876..a558ee0a42542 100644
--- a/nixos/tests/nsd.nix
+++ b/nixos/tests/nsd.nix
@@ -7,7 +7,7 @@ let
   };
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "nsd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aszlig ];
   };
 
diff --git a/nixos/tests/nzbget.nix b/nixos/tests/nzbget.nix
index 12d8ed6ea8da1..d6111ba079c85 100644
--- a/nixos/tests/nzbget.nix
+++ b/nixos/tests/nzbget.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "nzbget";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aanderse flokli ];
   };
 
@@ -10,7 +10,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
       # hack, don't add (unfree) unrar to nzbget's path,
       # so we can run this test in CI
-      systemd.services.nzbget.path = pkgs.stdenv.lib.mkForce [ pkgs.p7zip ];
+      systemd.services.nzbget.path = pkgs.lib.mkForce [ pkgs.p7zip ];
     };
   };
 
@@ -21,7 +21,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     server.wait_for_unit("network.target")
     server.wait_for_open_port(6789)
     assert "This file is part of nzbget" in server.succeed(
-        "curl -s -u nzbget:tegbzn6789 http://127.0.0.1:6789"
+        "curl -f -s -u nzbget:tegbzn6789 http://127.0.0.1:6789"
     )
     server.succeed(
         "${pkgs.nzbget}/bin/nzbget -n -o Control_iP=127.0.0.1 -o Control_port=6789 -o Control_password=tegbzn6789 -V"
diff --git a/nixos/tests/nzbhydra2.nix b/nixos/tests/nzbhydra2.nix
new file mode 100644
index 0000000000000..c82c756c3a1ce
--- /dev/null
+++ b/nixos/tests/nzbhydra2.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+  with lib;
+
+  {
+    name = "nzbhydra2";
+    meta.maintainers = with maintainers; [ jamiemagee ];
+
+    nodes.machine = { pkgs, ... }: { services.nzbhydra2.enable = true; };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("nzbhydra2.service")
+      machine.wait_for_open_port(5076)
+      machine.succeed("curl --fail http://localhost:5076/")
+    '';
+  })
diff --git a/nixos/tests/oci-containers.nix b/nixos/tests/oci-containers.nix
index bb6c019f07c99..0dfc7ffb276be 100644
--- a/nixos/tests/oci-containers.nix
+++ b/nixos/tests/oci-containers.nix
@@ -32,7 +32,7 @@ let
       start_all()
       ${backend}.wait_for_unit("${backend}-nginx.service")
       ${backend}.wait_for_open_port(8181)
-      ${backend}.wait_until_succeeds("curl http://localhost:8181 | grep Hello")
+      ${backend}.wait_until_succeeds("curl -f http://localhost:8181 | grep Hello")
     '';
   };
 
diff --git a/nixos/tests/oh-my-zsh.nix b/nixos/tests/oh-my-zsh.nix
new file mode 100644
index 0000000000000..57a073b086e88
--- /dev/null
+++ b/nixos/tests/oh-my-zsh.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "oh-my-zsh";
+
+  machine = { pkgs, ... }:
+
+    {
+      programs.zsh = {
+        enable = true;
+        ohMyZsh.enable = true;
+      };
+    };
+
+  testScript = ''
+    start_all()
+    machine.succeed("touch ~/.zshrc")
+    machine.succeed("zsh -c 'source /etc/zshrc && echo $ZSH | grep oh-my-zsh-${pkgs.oh-my-zsh.version}'")
+  '';
+})
diff --git a/nixos/tests/openarena.nix b/nixos/tests/openarena.nix
index 395ed9153ea11..461a35e89fe79 100644
--- a/nixos/tests/openarena.nix
+++ b/nixos/tests/openarena.nix
@@ -11,7 +11,7 @@ let
 
 in {
   name = "openarena";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ fpletz ];
   };
 
diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix
index f8321a2c522dc..f1a39ad7dde2f 100644
--- a/nixos/tests/openldap.nix
+++ b/nixos/tests/openldap.nix
@@ -1,33 +1,130 @@
-import ./make-test-python.nix {
-  name = "openldap";
-
-  machine = { pkgs, ... }: {
-    services.openldap = {
-      enable = true;
-      suffix = "dc=example";
-      rootdn = "cn=root,dc=example";
-      rootpw = "notapassword";
-      database = "bdb";
-      extraDatabaseConfig = ''
-        directory /var/db/openldap
-      '';
-      declarativeContents = ''
-        dn: dc=example
-        objectClass: domain
-        dc: example
-
-        dn: ou=users,dc=example
-        objectClass: organizationalUnit
-        ou: users
-      '';
-    };
-  };
+{ pkgs ? (import ../.. { inherit system; config = { }; })
+, system ? builtins.currentSystem
+, ...
+}:
 
+let
+  dbContents = ''
+    dn: dc=example
+    objectClass: domain
+    dc: example
+
+    dn: ou=users,dc=example
+    objectClass: organizationalUnit
+    ou: users
+  '';
   testScript = ''
     machine.wait_for_unit("openldap.service")
     machine.succeed(
-        "systemctl status openldap.service",
         'ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"',
     )
   '';
+in {
+  # New-style configuration
+  current = import ./make-test-python.nix ({ pkgs, ... }: {
+    inherit testScript;
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      environment.etc."openldap/root_password".text = "notapassword";
+      services.openldap = {
+        enable = true;
+        settings = {
+          children = {
+            "cn=schema".includes = [
+              "${pkgs.openldap}/etc/schema/core.ldif"
+              "${pkgs.openldap}/etc/schema/cosine.ldif"
+              "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              "${pkgs.openldap}/etc/schema/nis.ldif"
+            ];
+            "olcDatabase={1}mdb" = {
+              # This tests string, base64 and path values, as well as lists of string values
+              attrs = {
+                objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                olcDatabase = "{1}mdb";
+                olcDbDirectory = "/var/db/openldap";
+                olcSuffix = "dc=example";
+                olcRootDN = {
+                  # cn=root,dc=example
+                  base64 = "Y249cm9vdCxkYz1leGFtcGxl";
+                };
+                olcRootPW = {
+                  path = "/etc/openldap/root_password";
+                };
+              };
+            };
+          };
+        };
+        declarativeContents."dc=example" = dbContents;
+      };
+    };
+  }) { inherit pkgs system; };
+
+  # Old-style configuration
+  oldOptions = import ./make-test-python.nix ({ pkgs, ... }: {
+    inherit testScript;
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      services.openldap = {
+        enable = true;
+        logLevel = "stats acl";
+        defaultSchemas = true;
+        database = "mdb";
+        suffix = "dc=example";
+        rootdn = "cn=root,dc=example";
+        rootpw = "notapassword";
+        declarativeContents."dc=example" = dbContents;
+      };
+    };
+  }) { inherit system pkgs; };
+
+  # Manually managed configDir, for example if dynamic config is essential
+  manualConfigDir = import ./make-test-python.nix ({ pkgs, ... }: {
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      services.openldap = {
+        enable = true;
+        configDir = "/var/db/slapd.d";
+      };
+    };
+
+    testScript = let
+      contents = pkgs.writeText "data.ldif" dbContents;
+      config = pkgs.writeText "config.ldif" ''
+        dn: cn=config
+        cn: config
+        objectClass: olcGlobal
+        olcLogLevel: stats
+        olcPidFile: /run/slapd/slapd.pid
+
+        dn: cn=schema,cn=config
+        cn: schema
+        objectClass: olcSchemaConfig
+
+        include: file://${pkgs.openldap}/etc/schema/core.ldif
+        include: file://${pkgs.openldap}/etc/schema/cosine.ldif
+        include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif
+
+        dn: olcDatabase={1}mdb,cn=config
+        objectClass: olcDatabaseConfig
+        objectClass: olcMdbConfig
+        olcDatabase: {1}mdb
+        olcDbDirectory: /var/db/openldap
+        olcDbIndex: objectClass eq
+        olcSuffix: dc=example
+        olcRootDN: cn=root,dc=example
+        olcRootPW: notapassword
+      '';
+    in ''
+      machine.succeed(
+          "mkdir -p /var/db/slapd.d /var/db/openldap",
+          "slapadd -F /var/db/slapd.d -n0 -l ${config}",
+          "slapadd -F /var/db/slapd.d -n1 -l ${contents}",
+          "chown -R openldap:openldap /var/db/slapd.d /var/db/openldap",
+          "systemctl restart openldap",
+      )
+    '' + testScript;
+  }) { inherit system pkgs; };
 }
diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix
index e9692b5032721..003813379e697 100644
--- a/nixos/tests/openssh.nix
+++ b/nixos/tests/openssh.nix
@@ -4,7 +4,7 @@ let inherit (import ./ssh-keys.nix pkgs)
       snakeOilPrivateKey snakeOilPublicKey;
 in {
   name = "openssh";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ aszlig eelco ];
   };
 
diff --git a/nixos/tests/opentabletdriver.nix b/nixos/tests/opentabletdriver.nix
new file mode 100644
index 0000000000000..fe345a7bec735
--- /dev/null
+++ b/nixos/tests/opentabletdriver.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ( { pkgs, ... }: let
+  testUser = "alice";
+in {
+  name = "opentabletdriver";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ thiagokokada ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      imports = [
+        ./common/user-account.nix
+        ./common/x11.nix
+      ];
+      test-support.displayManager.auto.user = testUser;
+      hardware.opentabletdriver.enable = true;
+    };
+
+  testScript =
+    ''
+      machine.start()
+      machine.wait_for_x()
+      machine.wait_for_unit("opentabletdriver.service", "${testUser}")
+
+      machine.succeed("cat /etc/udev/rules.d/99-opentabletdriver.rules")
+      # Will fail if service is not running
+      # Needs to run as the same user that started the service
+      machine.succeed("su - ${testUser} -c 'otd detect'")
+    '';
+})
diff --git a/nixos/tests/orangefs.nix b/nixos/tests/orangefs.nix
index 24b7737058c83..fe9f9cc37ea03 100644
--- a/nixos/tests/orangefs.nix
+++ b/nixos/tests/orangefs.nix
@@ -9,7 +9,7 @@ let
 
     virtualisation.emptyDiskImages = [ 4096 ];
 
-    fileSystems = pkgs.lib.mkVMOverride
+    virtualisation.fileSystems =
       { "/data" =
           { device = "/dev/disk/by-label/data";
             fsType = "ext4";
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
index be0235a417536..f778d30bdc06c 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -9,7 +9,7 @@ let
       ${parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
       mkdir /mnt
       ${e2fsprogs}/bin/mkfs.ext4 /dev/vda1
-      ${utillinux}/bin/mount -t ext4 /dev/vda1 /mnt
+      ${util-linux}/bin/mount -t ext4 /dev/vda1 /mnt
 
       if test -e /mnt/.debug; then
         exec ${bash}/bin/sh
diff --git a/nixos/tests/osrm-backend.nix b/nixos/tests/osrm-backend.nix
index db67a5a589f9d..4067d5b1a239a 100644
--- a/nixos/tests/osrm-backend.nix
+++ b/nixos/tests/osrm-backend.nix
@@ -48,10 +48,10 @@ in {
     machine.wait_for_unit("osrm.service")
     machine.wait_for_open_port(${toString port})
     assert "Boulevard Rainier III" in machine.succeed(
-        "curl --silent '${query}' | jq .waypoints[0].name"
+        "curl --fail --silent '${query}' | jq .waypoints[0].name"
     )
     assert "Avenue de la Costa" in machine.succeed(
-        "curl --silent '${query}' | jq .waypoints[1].name"
+        "curl --fail --silent '${query}' | jq .waypoints[1].name"
     )
   '';
 })
diff --git a/nixos/tests/overlayfs.nix b/nixos/tests/overlayfs.nix
index 33794deb9ed83..142e7d378b2f5 100644
--- a/nixos/tests/overlayfs.nix
+++ b/nixos/tests/overlayfs.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "overlayfs";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ bachp ];
+  meta.maintainers = with pkgs.lib.maintainers; [ bachp ];
 
   machine = { pkgs, ... }: {
     virtualisation.emptyDiskImages = [ 512 ];
diff --git a/nixos/tests/packagekit.nix b/nixos/tests/packagekit.nix
index 7e93ad35e80a8..28d1374bf92cd 100644
--- a/nixos/tests/packagekit.nix
+++ b/nixos/tests/packagekit.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "packagekit";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ peterhoeg ];
   };
 
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
index c0434f20754ce..3894440333c99 100644
--- a/nixos/tests/pantheon.nix
+++ b/nixos/tests/pantheon.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 {
   name = "pantheon";
 
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = pkgs.pantheon.maintainers;
   };
 
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
index 355e7041d3fe2..fb83e6f976de6 100644
--- a/nixos/tests/paperless.nix
+++ b/nixos/tests/paperless.nix
@@ -23,14 +23,14 @@ import ./make-test-python.nix ({ lib, ... } : {
     with subtest("Service gets ready"):
         machine.wait_for_unit("paperless-server.service")
         # Wait until server accepts connections
-        machine.wait_until_succeeds("curl -s localhost:28981")
+        machine.wait_until_succeeds("curl -fs localhost:28981")
 
     with subtest("Test document is consumed"):
         machine.wait_until_succeeds(
-            "(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))"
+            "(($(curl -fs localhost:28981/api/documents/ | jq .count) == 1))"
         )
         assert "2005-10-16" in machine.succeed(
-            "curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
+            "curl -fs localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
         )
   '';
 })
diff --git a/nixos/tests/peerflix.nix b/nixos/tests/peerflix.nix
index 37628604d49b5..4800413783b1b 100644
--- a/nixos/tests/peerflix.nix
+++ b/nixos/tests/peerflix.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "peerflix";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ offline ];
   };
 
@@ -18,6 +18,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     start_all()
 
     peerflix.wait_for_unit("peerflix.service")
-    peerflix.wait_until_succeeds("curl localhost:9000")
+    peerflix.wait_until_succeeds("curl -f localhost:9000")
   '';
 })
diff --git a/nixos/tests/pgmanage.nix b/nixos/tests/pgmanage.nix
index 4f5dbed24a97f..6f8f2f9653408 100644
--- a/nixos/tests/pgmanage.nix
+++ b/nixos/tests/pgmanage.nix
@@ -6,7 +6,7 @@ let
 in
 {
   name = "pgmanage";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ basvandijk ];
   };
   nodes = {
diff --git a/nixos/tests/php/fpm.nix b/nixos/tests/php/fpm.nix
index 513abd9437378..9ad515ebdde06 100644
--- a/nixos/tests/php/fpm.nix
+++ b/nixos/tests/php/fpm.nix
@@ -43,7 +43,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
     machine.wait_for_unit("phpfpm-foobar.service")
 
     # Check so we get an evaluated PHP back
-    response = machine.succeed("curl -vvv -s http://127.0.0.1:80/")
+    response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
     assert "PHP Version ${pkgs.php.version}" in response, "PHP version not detected"
 
     # Check so we have database and some other extensions loaded
diff --git a/nixos/tests/php/httpd.nix b/nixos/tests/php/httpd.nix
index 1092e0ecadd39..27ea7a24e3a99 100644
--- a/nixos/tests/php/httpd.nix
+++ b/nixos/tests/php/httpd.nix
@@ -21,7 +21,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
     machine.wait_for_unit("httpd.service")
 
     # Check so we get an evaluated PHP back
-    response = machine.succeed("curl -vvv -s http://127.0.0.1:80/")
+    response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
     assert "PHP Version ${pkgs.php.version}" in response, "PHP version not detected"
 
     # Check so we have database and some other extensions loaded
diff --git a/nixos/tests/php/pcre.nix b/nixos/tests/php/pcre.nix
index 3dd0964e60fbe..3ea19304bffd5 100644
--- a/nixos/tests/php/pcre.nix
+++ b/nixos/tests/php/pcre.nix
@@ -32,7 +32,7 @@ in import ../make-test-python.nix ({lib, ...}: {
     ''
       machine.wait_for_unit("httpd.service")
       # Ensure php evaluation by matching on the var_dump syntax
-      response = machine.succeed("curl -vvv -s http://127.0.0.1:80/index.php")
+      response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/index.php")
       expected = 'string(${toString (builtins.stringLength testString)}) "${testString}"'
       assert expected in response, "Does not appear to be able to use subgroups."
     '';
diff --git a/nixos/tests/pinnwand.nix b/nixos/tests/pinnwand.nix
index 2204e74b2c286..0c583e1104dec 100644
--- a/nixos/tests/pinnwand.nix
+++ b/nixos/tests/pinnwand.nix
@@ -25,7 +25,7 @@ let
 in
 {
   name = "pinnwand";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers =[ hexa ];
   };
 
diff --git a/nixos/tests/plasma5.nix b/nixos/tests/plasma5.nix
index 5a603f8cbfb5b..f09859a055d5c 100644
--- a/nixos/tests/plasma5.nix
+++ b/nixos/tests/plasma5.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
 {
   name = "plasma5";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ttuegel ];
   };
 
@@ -35,6 +35,9 @@ import ./make-test-python.nix ({ pkgs, ...} :
         machine.wait_until_succeeds("pgrep plasmashell")
         machine.wait_for_window("^Desktop ")
 
+    with subtest("Check that KDED is running"):
+        machine.succeed("pgrep kded5")
+
     with subtest("Check that logging in has given the user ownership of devices"):
         machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
 
diff --git a/nixos/tests/pleroma.nix b/nixos/tests/pleroma.nix
new file mode 100644
index 0000000000000..797cac44f9562
--- /dev/null
+++ b/nixos/tests/pleroma.nix
@@ -0,0 +1,265 @@
+/*
+  Pleroma E2E VM test.
+
+  Abstract:
+  =========
+  Using pleroma, postgresql, a local CA cert, a nginx reverse proxy
+  and a toot-based client, we're going to:
+
+  1. Provision a pleroma service from scratch (pleroma config + postgres db).
+  2. Create a "jamy" admin user.
+  3. Send a toot from this user.
+  4. Send a upload from this user.
+  5. Check the toot is part of the server public timeline
+
+  Notes:
+  - We need a fully functional TLS setup without having any access to
+    the internet. We do that by issuing a self-signed cert, add this
+    self-cert to the hosts pki trust store and finally spoof the
+    hostnames using /etc/hosts.
+  - For this NixOS test, we *had* to store some DB-related and
+    pleroma-related secrets to the store. Keep in mind the store is
+    world-readable, it's the worst place possible to store *any*
+    secret. **DO NOT DO THIS IN A REAL WORLD DEPLOYMENT**.
+*/
+
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+  send-toot = pkgs.writeScriptBin "send-toot" ''
+    set -eux
+    # toot is using the requests library internally. This library
+    # sadly embed its own certificate store instead of relying on the
+    # system one. Overriding this pretty bad default behaviour.
+    export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
+
+    export TOOT_LOGIN_CLI_PASSWORD="jamy-password"
+    toot login_cli -i "pleroma.nixos.test" -e "jamy@nixos.test"
+    echo "Login OK"
+
+    # Send a toot then verify it's part of the public timeline
+    echo "y" | toot post "hello world Jamy here"
+    echo "Send toot OK"
+    echo "y" | toot timeline | grep -c "hello world Jamy here"
+    echo "Get toot from timeline OK"
+
+    # Test file upload
+    echo "y" | toot upload ${db-seed} | grep -c "https://pleroma.nixos.test/media"
+    echo "File upload OK"
+
+    echo "====================================================="
+    echo "=                   SUCCESS                         ="
+    echo "=                                                   ="
+    echo "=    We were able to sent a toot + a upload and     ="
+    echo "=   retrieve both of them in the public timeline.   ="
+    echo "====================================================="
+  '';
+
+  provision-db = pkgs.writeScriptBin "provision-db" ''
+    set -eux
+    sudo -u postgres psql -f ${db-seed}
+  '';
+
+  test-db-passwd = "SccZOvTGM//BMrpoQj68JJkjDkMGb4pHv2cECWiI+XhVe3uGJTLI0vFV/gDlZ5jJ";
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.*/
+  db-seed = pkgs.writeText "provision.psql" ''
+    CREATE USER pleroma WITH ENCRYPTED PASSWORD '${test-db-passwd}';
+    CREATE DATABASE pleroma OWNER pleroma;
+    \c pleroma;
+    --Extensions made by ecto.migrate that need superuser access
+    CREATE EXTENSION IF NOT EXISTS citext;
+    CREATE EXTENSION IF NOT EXISTS pg_trgm;
+    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+  '';
+
+  pleroma-conf = ''
+    import Config
+
+    config :pleroma, Pleroma.Web.Endpoint,
+       url: [host: "pleroma.nixos.test", scheme: "https", port: 443],
+       http: [ip: {127, 0, 0, 1}, port: 4000]
+
+    config :pleroma, :instance,
+      name: "NixOS test pleroma server",
+      email: "pleroma@nixos.test",
+      notify_email: "pleroma@nixos.test",
+      limit: 5000,
+      registrations_open: true
+
+    config :pleroma, :media_proxy,
+      enabled: false,
+      redirect_on_failure: true
+      #base_url: "https://cache.pleroma.social"
+
+    config :pleroma, Pleroma.Repo,
+      adapter: Ecto.Adapters.Postgres,
+      username: "pleroma",
+      password: "${test-db-passwd}",
+      database: "pleroma",
+      hostname: "localhost",
+      pool_size: 10,
+      prepare: :named,
+      parameters: [
+        plan_cache_mode: "force_custom_plan"
+      ]
+
+    config :pleroma, :database, rum_enabled: false
+    config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
+    config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
+    config :pleroma, configurable_from_database: false
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+    In a real-word deployment, you'd handle this either by:
+    - manually upload your pleroma secrets to /var/lib/pleroma/secrets.exs
+    - use a deployment tool such as morph or NixOps to deploy your secrets.
+  */
+  pleroma-conf-secret = pkgs.writeText "secrets.exs" ''
+    import Config
+
+    config :joken, default_signer: "PS69/wMW7X6FIQPABt9lwvlZvgrJIncfiAMrK9J5mjVus/7/NJJi1DsDA1OghBE5"
+
+    config :pleroma, Pleroma.Web.Endpoint,
+       secret_key_base: "NvfmU7lYaQrmmxt4NACm0AaAfN9t6WxsrX0NCB4awkGHvr1S7jyshlEmrjaPFhhq",
+       signing_salt: "3L41+BuJ"
+
+    config :web_push_encryption, :vapid_details,
+      subject: "mailto:pleroma@nixos.test",
+      public_key: "BKjfNX9-UqAcncaNqERQtF7n9pKrB0-MO-juv6U5E5XQr_Tg5D-f8AlRjduAguDpyAngeDzG8MdrTejMSL4VF30",
+      private_key: "k7o9onKMQrgMjMb6l4fsxSaXO0BTNAer5MVSje3q60k"
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+    In a real-word deployment, you'd handle this either by:
+    - manually upload your pleroma secrets to /var/lib/pleroma/secrets.exs
+    - use a deployment tool such as morph or NixOps to deploy your secrets.
+    */
+  provision-secrets = pkgs.writeScriptBin "provision-secrets" ''
+    set -eux
+    cp "${pleroma-conf-secret}" "/var/lib/pleroma/secrets.exs"
+    chown pleroma:pleroma /var/lib/pleroma/secrets.exs
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+  */
+  provision-user = pkgs.writeScriptBin "provision-user" ''
+    set -eux
+
+    # Waiting for pleroma to be up.
+    timeout 5m bash -c 'while [[ "$(curl -s -o /dev/null -w '%{http_code}' https://pleroma.nixos.test/api/v1/instance)" != "200" ]]; do sleep 2; done'
+    pleroma_ctl user new jamy jamy@nixos.test --password 'jamy-password' --moderator --admin -y
+  '';
+
+  tls-cert = pkgs.runCommandNoCC "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=pleroma.nixos.test'
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+
+  /* Toot is preventing users from feeding login_cli a password non
+     interactively. While it makes sense most of the times, it's
+     preventing us to login in this non-interactive test. This patch
+     introduce a TOOT_LOGIN_CLI_PASSWORD env variable allowing us to
+     provide a password to toot login_cli
+
+     If https://github.com/ihabunek/toot/pull/180 gets merged at some
+     point, feel free to remove this patch. */
+  custom-toot = pkgs.toot.overrideAttrs(old:{
+    patches = [ (pkgs.fetchpatch {
+      url = "https://github.com/NinjaTrappeur/toot/commit/b4a4c30f41c0cb7e336714c2c4af9bc9bfa0c9f2.patch";
+      sha256 = "sha256-0xxNwjR/fStLjjUUhwzCCfrghRVts+fc+fvVJqVcaFg=";
+    }) ];
+  });
+
+  hosts = nodes: ''
+    ${nodes.pleroma.config.networking.primaryIPAddress} pleroma.nixos.test
+    ${nodes.client.config.networking.primaryIPAddress} client.nixos.test
+  '';
+  in {
+  name = "pleroma";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+      networking.extraHosts = hosts nodes;
+      environment.systemPackages = with pkgs; [
+        custom-toot
+        send-toot
+      ];
+    };
+    pleroma = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+      networking.extraHosts = hosts nodes;
+      networking.firewall.enable = false;
+      environment.systemPackages = with pkgs; [
+        provision-db
+        provision-secrets
+        provision-user
+      ];
+      services = {
+        pleroma = {
+          enable = true;
+          configs = [
+            pleroma-conf
+          ];
+        };
+        postgresql = {
+          enable = true;
+          package = pkgs.postgresql_12;
+        };
+        nginx = {
+          enable = true;
+          virtualHosts."pleroma.nixos.test" = {
+            addSSL = true;
+            sslCertificate = "${tls-cert}/cert.pem";
+            sslCertificateKey = "${tls-cert}/key.pem";
+            locations."/" = {
+              proxyPass = "http://127.0.0.1:4000";
+              extraConfig = ''
+                add_header 'Access-Control-Allow-Origin' '*' always;
+                add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+                add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+                add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+                if ($request_method = OPTIONS) {
+                    return 204;
+                }
+                add_header X-XSS-Protection "1; mode=block";
+                add_header X-Permitted-Cross-Domain-Policies none;
+                add_header X-Frame-Options DENY;
+                add_header X-Content-Type-Options nosniff;
+                add_header Referrer-Policy same-origin;
+                add_header X-Download-Options noopen;
+                proxy_http_version 1.1;
+                proxy_set_header Upgrade $http_upgrade;
+                proxy_set_header Connection "upgrade";
+                proxy_set_header Host $host;
+                client_max_body_size 16m;
+              '';
+            };
+          };
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    pleroma.wait_for_unit("postgresql.service")
+    pleroma.succeed("provision-db")
+    pleroma.succeed("provision-secrets")
+    pleroma.systemctl("restart pleroma.service")
+    pleroma.wait_for_unit("pleroma.service")
+    pleroma.succeed("provision-user")
+    client.succeed("send-toot")
+  '';
+})
diff --git a/nixos/tests/podman.nix b/nixos/tests/podman.nix
index cd8c2b4308c8d..4985ff60365c9 100644
--- a/nixos/tests/podman.nix
+++ b/nixos/tests/podman.nix
@@ -34,7 +34,6 @@ import ./make-test-python.nix (
       podman.wait_for_unit("sockets.target")
       start_all()
 
-
       with subtest("Run container as root with runc"):
           podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
@@ -53,6 +52,18 @@ import ./make-test-python.nix (
           podman.succeed("podman stop sleeping")
           podman.succeed("podman rm sleeping")
 
+      with subtest("Run container as root with the default backend"):
+          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.succeed("podman ps | grep sleeping")
+          podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
+
+      # create systemd session for rootless
+      podman.succeed("loginctl enable-linger alice")
+
       with subtest("Run container rootless with runc"):
           podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
           podman.succeed(
@@ -74,6 +85,17 @@ import ./make-test-python.nix (
           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 the default backend"):
+          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.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.nix b/nixos/tests/postfix.nix
index 37ae76afec107..6d22b4edba0a2 100644
--- a/nixos/tests/postfix.nix
+++ b/nixos/tests/postfix.nix
@@ -11,9 +11,9 @@ import ./make-test-python.nix {
       enable = true;
       enableSubmission = true;
       enableSubmissions = true;
-      sslCACert = certs.ca.cert;
-      sslCert = certs.${domain}.cert;
-      sslKey = certs.${domain}.key;
+      tlsTrustedAuthorities = "${certs.ca.cert}";
+      sslCert = "${certs.${domain}.cert}";
+      sslKey = "${certs.${domain}.key}";
       submissionsOptions = {
           smtpd_sasl_auth_enable = "yes";
           smtpd_client_restrictions = "permit";
diff --git a/nixos/tests/postgis.nix b/nixos/tests/postgis.nix
index 84bbb0bc8ec63..9d81ebaad85f2 100644
--- a/nixos/tests/postgis.nix
+++ b/nixos/tests/postgis.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "postgis";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lsix ];
   };
 
diff --git a/nixos/tests/postgresql-wal-receiver.nix b/nixos/tests/postgresql-wal-receiver.nix
index 432b46234f9c1..0e8b3bfd6c34f 100644
--- a/nixos/tests/postgresql-wal-receiver.nix
+++ b/nixos/tests/postgresql-wal-receiver.nix
@@ -1,11 +1,19 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
 let
+  lib = pkgs.lib;
+
   # Makes a test for a PostgreSQL package, given by name and looked up from `pkgs`.
   makePostgresqlWalReceiverTest = postgresqlPackage:
   {
     name = postgresqlPackage;
     value =
-      import ./make-test-python.nix ({ pkgs, lib, ... }: let
-
+      let
         pkg = pkgs."${postgresqlPackage}";
         postgresqlDataDir = "/var/lib/postgresql/${pkg.psqlSchema}";
         replicationUser = "wal_receiver_user";
@@ -19,7 +27,7 @@ let
             then pkgs.writeTextDir "recovery.signal" ""
             else pkgs.writeTextDir "recovery.conf" "restore_command = 'cp ${walBackupDir}/%f %p'";
 
-      in {
+      in makeTest {
         name = "postgresql-wal-receiver-${postgresqlPackage}";
         meta.maintainers = with lib.maintainers; [ pacien ];
 
@@ -104,7 +112,7 @@ let
               "test $(sudo -u postgres psql --pset='pager=off' --tuples-only --command='select count(distinct val) from dummy;') -eq 100"
           )
         '';
-      });
+      };
     };
 
 # Maps the generic function over all attributes of PostgreSQL packages
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index 3201e22555eab..091e64294ac5b 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -23,7 +23,7 @@ let
   '';
   make-postgresql-test = postgresql-name: postgresql-package: backup-all: makeTest {
     name = postgresql-name;
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ zagy ];
     };
 
diff --git a/nixos/tests/power-profiles-daemon.nix b/nixos/tests/power-profiles-daemon.nix
new file mode 100644
index 0000000000000..e073677bee9d7
--- /dev/null
+++ b/nixos/tests/power-profiles-daemon.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "power-profiles-daemon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mvnetbiz ];
+  };
+  machine = { pkgs, ... }: {
+    services.power-profiles-daemon.enable = true;
+    environment.systemPackages = [ pkgs.glib ];
+  };
+
+  testScript = ''
+    def get_profile():
+        return machine.succeed(
+            """gdbus call --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles \
+    --method org.freedesktop.DBus.Properties.Get 'net.hadess.PowerProfiles' 'ActiveProfile'
+    """
+        )
+
+
+    def set_profile(profile):
+        return machine.succeed(
+            """gdbus call --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles \
+    --method org.freedesktop.DBus.Properties.Set 'net.hadess.PowerProfiles' 'ActiveProfile' "<'{profile}'>"
+    """.format(
+                profile=profile
+            )
+        )
+
+
+    machine.wait_for_unit("multi-user.target")
+
+    set_profile("power-saver")
+    profile = get_profile()
+    if not "power-saver" in profile:
+        raise Exception("Unable to set power-saver profile")
+
+
+    set_profile("balanced")
+    profile = get_profile()
+    if not "balanced" in profile:
+        raise Exception("Unable to set balanced profile")
+  '';
+})
diff --git a/nixos/tests/powerdns.nix b/nixos/tests/powerdns.nix
index 75d71315e644d..d025934ad2b37 100644
--- a/nixos/tests/powerdns.nix
+++ b/nixos/tests/powerdns.nix
@@ -1,13 +1,65 @@
-import ./make-test-python.nix ({ pkgs, ... }: {
+# This test runs PowerDNS authoritative server with the
+# generic MySQL backend (gmysql) to connect to a
+# MariaDB server using UNIX sockets authentication.
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "powerdns";
 
   nodes.server = { ... }: {
     services.powerdns.enable = true;
-    environment.systemPackages = [ pkgs.dnsutils ];
+    services.powerdns.extraConfig = ''
+      launch=gmysql
+      gmysql-user=pdns
+    '';
+
+    services.mysql = {
+      enable = true;
+      package = pkgs.mariadb;
+      ensureDatabases = [ "powerdns" ];
+      ensureUsers = lib.singleton
+        { name = "pdns";
+          ensurePermissions = { "powerdns.*" = "ALL PRIVILEGES"; };
+        };
+    };
+
+    environment.systemPackages = with pkgs;
+      [ dnsutils powerdns mariadb ];
   };
 
   testScript = ''
-    server.wait_for_unit("pdns")
-    server.succeed("dig version.bind txt chaos \@127.0.0.1")
+    import re
+
+    with subtest("PowerDNS database exists"):
+        server.wait_for_unit("mysql")
+        server.succeed("echo 'SHOW DATABASES;' | sudo -u pdns mysql -u pdns >&2")
+
+    with subtest("Loading the MySQL schema works"):
+        server.succeed(
+            "sudo -u pdns mysql -u pdns -D powerdns <"
+            "${pkgs.powerdns}/share/doc/pdns/schema.mysql.sql"
+        )
+
+    with subtest("PowerDNS server starts"):
+        server.wait_for_unit("pdns")
+        server.succeed("dig version.bind txt chaos @127.0.0.1 >&2")
+
+    with subtest("Adding an example zone works"):
+        # Extract configuration file needed by pdnsutil
+        unit = server.succeed("systemctl cat pdns")
+        conf = re.search("(--config-dir=[^ ]+)", unit).group(1)
+        pdnsutil = "sudo -u pdns pdnsutil " + conf
+        server.succeed(f"{pdnsutil} create-zone example.com ns1.example.com")
+        server.succeed(f"{pdnsutil} add-record  example.com ns1 A 192.168.1.2")
+
+    with subtest("Querying the example zone works"):
+        reply = server.succeed("dig +noall +answer ns1.example.com @127.0.0.1")
+        assert (
+            "192.168.1.2" in reply
+        ), f""""
+        The reply does not contain the expected IP address:
+          Expected:
+            ns1.example.com.        3600    IN      A       192.168.1.2
+          Reply:
+            {reply}"""
   '';
 })
diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix
index bab091d57acf1..c0b472638a14d 100644
--- a/nixos/tests/predictable-interface-names.nix
+++ b/nixos/tests/predictable-interface-names.nix
@@ -5,7 +5,11 @@
 
 let
   inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
-in pkgs.lib.listToAttrs (pkgs.lib.crossLists (predictable: withNetworkd: {
+  testCombinations = pkgs.lib.cartesianProductOfSets {
+    predictable = [true false];
+    withNetworkd = [true false];
+  };
+in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd }: {
   name = pkgs.lib.optionalString (!predictable) "un" + "predictable"
        + pkgs.lib.optionalString withNetworkd "Networkd";
   value = makeTest {
@@ -30,4 +34,4 @@ in pkgs.lib.listToAttrs (pkgs.lib.crossLists (predictable: withNetworkd: {
       machine.${if predictable then "fail" else "succeed"}("ip link show eth0")
     '';
   };
-}) [[true false] [true false]])
+}) testCombinations)
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
index 355c94a038616..6a1801fb28840 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -35,7 +35,7 @@ let
 
 in {
   name = "printing";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ domenkozar eelco matthewbauer ];
   };
 
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
index 45c7cd37c241f..b71ff0a1669fe 100644
--- a/nixos/tests/privacyidea.nix
+++ b/nixos/tests/privacyidea.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : rec {
   name = "privacyidea";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ fpletz ];
   };
 
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index ad2fff2b01f66..89d17c9de8c06 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -96,6 +96,31 @@ let
       '';
     };
 
+    bird = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.bird2.enable = true;
+        services.bird2.config = ''
+          protocol kernel MyObviousTestString {
+            ipv4 {
+              import all;
+              export none;
+            };
+          }
+
+          protocol device {
+          }
+        '';
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-bird-exporter.service")
+        wait_for_open_port(9324)
+        succeed("curl -sSf http://localhost:9324/metrics | grep -q 'MyObviousTestString'")
+      '';
+    };
+
     blackbox = {
       exporterConfig = {
         enable = true;
@@ -197,10 +222,11 @@ let
       exporterConfig = {
         enable = true;
         url = "http://localhost";
-        configFile = pkgs.writeText "json-exporter-conf.json" (builtins.toJSON [{
-          name = "json_test_metric";
-          path = "$.test";
-        }]);
+        configFile = pkgs.writeText "json-exporter-conf.json" (builtins.toJSON {
+          metrics = [
+            { name = "json_test_metric"; path = "$.test"; }
+          ];
+        });
       };
       metricProvider = {
         systemd.services.prometheus-json-exporter.after = [ "nginx.service" ];
@@ -216,7 +242,9 @@ let
         wait_for_open_port(80)
         wait_for_unit("prometheus-json-exporter.service")
         wait_for_open_port(7979)
-        succeed("curl -sSf localhost:7979/metrics | grep -q 'json_test_metric 1'")
+        succeed(
+            "curl -sSf 'localhost:7979/probe?target=http://localhost' | grep -q 'json_test_metric 1'"
+        )
       '';
     };
 
@@ -395,7 +423,7 @@ let
       exporterConfig = {
         enable = true;
         passwordFile = "/var/nextcloud-pwfile";
-        url = "http://localhost/negative-space.xml";
+        url = "http://localhost";
       };
       metricProvider = {
         systemd.services.nc-pwfile = let
@@ -413,6 +441,7 @@ let
             basicAuth.nextcloud-exporter = "snakeoilpw";
             locations."/" = {
               root = "${pkgs.prometheus-nextcloud-exporter.src}/serverinfo/testdata";
+              tryFiles = "/negative-space.xml =404";
             };
           };
         };
@@ -444,6 +473,67 @@ let
       '';
     };
 
+    nginxlog = {
+      exporterConfig = {
+        enable = true;
+        group = "nginx";
+        settings = {
+          namespaces = [
+            {
+              name = "filelogger";
+              source = {
+                files = [ "/var/log/nginx/filelogger.access.log" ];
+              };
+            }
+            {
+              name = "syslogger";
+              source = {
+                syslog = {
+                  listen_address = "udp://127.0.0.1:10000";
+                  format = "rfc3164";
+                  tags = ["nginx"];
+                };
+              };
+            }
+          ];
+        };
+      };
+      metricProvider = {
+        services.nginx = {
+          enable = true;
+          httpConfig = ''
+            server {
+              listen 80;
+              server_name filelogger.local;
+              access_log /var/log/nginx/filelogger.access.log;
+            }
+            server {
+              listen 81;
+              server_name syslogger.local;
+              access_log syslog:server=127.0.0.1:10000,tag=nginx,severity=info;
+            }
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nginxlog-exporter.service")
+        wait_for_open_port(9117)
+        wait_for_open_port(80)
+        wait_for_open_port(81)
+        succeed("curl http://localhost")
+        execute("sleep 1")
+        succeed(
+            "curl -sSf http://localhost:9117/metrics | grep 'filelogger_http_response_count_total' | grep -q 1"
+        )
+        succeed("curl http://localhost:81")
+        execute("sleep 1")
+        succeed(
+            "curl -sSf http://localhost:9117/metrics | grep 'syslogger_http_response_count_total' | grep -q 1"
+        )
+      '';
+    };
+
     node = {
       exporterConfig = {
         enable = true;
@@ -530,6 +620,21 @@ let
       '';
     };
 
+    py-air-control = {
+      nodeName = "py_air_control";
+      exporterConfig = {
+        enable = true;
+        deviceHostname = "127.0.0.1";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-py-air-control-exporter.service")
+        wait_for_open_port(9896)
+        succeed(
+            "curl -sSf http://localhost:9896/metrics | grep -q 'py_air_control_sampling_error_total'"
+        )
+      '';
+    };
+
     redis = {
       exporterConfig = {
         enable = true;
@@ -558,7 +663,59 @@ let
         wait_for_open_port(11334)
         wait_for_open_port(7980)
         wait_until_succeeds(
-            "curl -sSf localhost:7980/metrics | grep -q 'rspamd_scanned{host=\"rspamd\"} 0'"
+            "curl -sSf 'localhost:7980/probe?target=http://localhost:11334/stat' | grep -q 'rspamd_scanned{host=\"rspamd\"} 0'"
+        )
+      '';
+    };
+
+    rtl_433 = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        # Mock rtl_433 binary to return a dummy metric stream.
+        nixpkgs.overlays = [ (self: super: {
+          rtl_433 = self.runCommand "rtl_433" {} ''
+            mkdir -p "$out/bin"
+            cat <<EOF > "$out/bin/rtl_433"
+            #!/bin/sh
+            while true; do
+              printf '{"time" : "2020-04-26 13:37:42", "model" : "zopieux", "id" : 55, "channel" : 3, "temperature_C" : 18.000}\n'
+              sleep 4
+            done
+            EOF
+            chmod +x "$out/bin/rtl_433"
+          '';
+        }) ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-rtl_433-exporter.service")
+        wait_for_open_port(9550)
+        wait_until_succeeds(
+            "curl -sSf localhost:9550/metrics | grep -q '{}'".format(
+                'rtl_433_temperature_celsius{channel="3",id="55",location="",model="zopieux"} 18'
+            )
+        )
+      '';
+    };
+
+    smokeping = {
+      exporterConfig = {
+        enable = true;
+        hosts = ["127.0.0.1"];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-smokeping-exporter.service")
+        wait_for_open_port(9374)
+        wait_until_succeeds(
+            "curl -sSf localhost:9374/metrics | grep '{}' | grep -qv ' 0$'".format(
+                'smokeping_requests_total{host="127.0.0.1",ip="127.0.0.1"} '
+            )
+        )
+        wait_until_succeeds(
+            "curl -sSf localhost:9374/metrics | grep -q '{}'".format(
+                'smokeping_response_ttl{host="127.0.0.1",ip="127.0.0.1"}'
+            )
         )
       '';
     };
@@ -578,6 +735,50 @@ let
       '';
     };
 
+    sql = {
+      exporterConfig = {
+        configuration.jobs.points = {
+          interval = "1m";
+          connections = [
+            "postgres://prometheus-sql-exporter@/data?host=/run/postgresql&sslmode=disable"
+          ];
+          queries = {
+            points = {
+              labels = [ "name" ];
+              help = "Amount of points accumulated per person";
+              values = [ "amount" ];
+              query = "SELECT SUM(amount) as amount, name FROM points GROUP BY name";
+            };
+          };
+        };
+        enable = true;
+        user = "prometheus-sql-exporter";
+      };
+      metricProvider = {
+        services.postgresql = {
+          enable = true;
+          initialScript = builtins.toFile "init.sql" ''
+            CREATE DATABASE data;
+            \c data;
+            CREATE TABLE points (amount INT, name TEXT);
+            INSERT INTO points(amount, name) VALUES (1, 'jack');
+            INSERT INTO points(amount, name) VALUES (2, 'jill');
+            INSERT INTO points(amount, name) VALUES (3, 'jack');
+
+            CREATE USER "prometheus-sql-exporter";
+            GRANT ALL PRIVILEGES ON DATABASE data TO "prometheus-sql-exporter";
+            GRANT SELECT ON points TO "prometheus-sql-exporter";
+          '';
+        };
+        systemd.services.prometheus-sql-exporter.after = [ "postgresql.service" ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-sql-exporter.service")
+        wait_for_open_port(9237)
+        succeed("curl http://localhost:9237/metrics | grep -c 'sql_points{' | grep -q 2")
+      '';
+    };
+
     surfboard = {
       exporterConfig = {
         enable = true;
diff --git a/nixos/tests/prometheus.nix b/nixos/tests/prometheus.nix
index af2aa66a55266..70ac78a4a4689 100644
--- a/nixos/tests/prometheus.nix
+++ b/nixos/tests/prometheus.nix
@@ -19,7 +19,6 @@ let
       secret_key = s3.secretKey;
       insecure = true;
       signature_version2 = false;
-      encrypt_sse =  false;
       put_user_metadata = {};
       http_config = {
         idle_conn_timeout = "0s";
@@ -37,6 +36,7 @@ in import ./make-test-python.nix {
   nodes = {
     prometheus = { pkgs, ... }: {
       virtualisation.diskSize = 2 * 1024;
+      virtualisation.memorySize = 2048;
       environment.systemPackages = [ pkgs.jq ];
       networking.firewall.allowedTCPPorts = [ grpcPort ];
       services.prometheus = {
@@ -133,6 +133,7 @@ in import ./make-test-python.nix {
 
     store = { pkgs, ... }: {
       virtualisation.diskSize = 2 * 1024;
+      virtualisation.memorySize = 2048;
       environment.systemPackages = with pkgs; [ jq thanos ];
       services.thanos.store = {
         enable = true;
@@ -193,13 +194,13 @@ in import ./make-test-python.nix {
     # Check if prometheus responds to requests:
     prometheus.wait_for_unit("prometheus.service")
     prometheus.wait_for_open_port(${toString queryPort})
-    prometheus.succeed("curl -s http://127.0.0.1:${toString queryPort}/metrics")
+    prometheus.succeed("curl -sf http://127.0.0.1:${toString queryPort}/metrics")
 
     # Let's test if pushing a metric to the pushgateway succeeds:
     prometheus.wait_for_unit("pushgateway.service")
     prometheus.succeed(
         "echo 'some_metric 3.14' | "
-        + "curl --data-binary \@- "
+        + "curl -f --data-binary \@- "
         + "http://127.0.0.1:${toString pushgwPort}/metrics/job/some_job"
     )
 
diff --git a/nixos/tests/proxy.nix b/nixos/tests/proxy.nix
index 6a14a9af59aec..f8a3d576903e3 100644
--- a/nixos/tests/proxy.nix
+++ b/nixos/tests/proxy.nix
@@ -11,7 +11,7 @@ let
   };
 in {
   name = "proxy";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/pt2-clone.nix b/nixos/tests/pt2-clone.nix
index b502172e2ee5e..3c090b7de4286 100644
--- a/nixos/tests/pt2-clone.nix
+++ b/nixos/tests/pt2-clone.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "pt2-clone";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ fgaz ];
   };
 
diff --git a/nixos/tests/quagga.nix b/nixos/tests/quagga.nix
index 04590aa0eb386..9aed49bf452f6 100644
--- a/nixos/tests/quagga.nix
+++ b/nixos/tests/quagga.nix
@@ -23,7 +23,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     {
       name = "quagga";
 
-      meta = with pkgs.stdenv.lib.maintainers; {
+      meta = with pkgs.lib.maintainers; {
         maintainers = [ tavyc ];
       };
 
diff --git a/nixos/tests/quorum.nix b/nixos/tests/quorum.nix
index 846d2a9301889..498b55ace7af5 100644
--- a/nixos/tests/quorum.nix
+++ b/nixos/tests/quorum.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "quorum";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
@@ -55,7 +55,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         parentHash =
           "0x0000000000000000000000000000000000000000000000000000000000000000";
         timestamp = "0x5cffc201";
-		  };
+      };
      };
     };
   };
diff --git a/nixos/tests/rabbitmq.nix b/nixos/tests/rabbitmq.nix
index f403e4ac2edc5..8a7fcc0e8991a 100644
--- a/nixos/tests/rabbitmq.nix
+++ b/nixos/tests/rabbitmq.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "rabbitmq";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco offline ];
   };
 
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index 529965d7acdee..ca17156143598 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -1,6 +1,10 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  redisSocket = "/run/redis/redis.sock";
+in
+{
   name = "redis";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ flokli ];
   };
 
@@ -10,7 +14,20 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
       {
         services.redis.enable = true;
-        services.redis.unixSocket = "/run/redis/redis.sock";
+        services.redis.unixSocket = redisSocket;
+
+        # Allow access to the unix socket for the "redis" group.
+        services.redis.settings.unixsocketperm = "770";
+
+        users.users."member" = {
+          createHome = false;
+          description = "A member of the redis group";
+          extraGroups = [
+            "redis"
+          ];
+          group = "users";
+          shell = "/bin/sh";
+        };
       };
   };
 
@@ -18,7 +35,11 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     start_all()
     machine.wait_for_unit("redis")
     machine.wait_for_open_port("6379")
+
+    # The unix socket is accessible to the redis group
+    machine.succeed('su member -c "redis-cli ping | grep PONG"')
+
     machine.succeed("redis-cli ping | grep PONG")
-    machine.succeed("redis-cli -s /run/redis/redis.sock ping | grep PONG")
+    machine.succeed("redis-cli -s ${redisSocket} ping | grep PONG")
   '';
 })
diff --git a/nixos/tests/resolv.nix b/nixos/tests/resolv.nix
index b506f87451ee7..f0aa7e42aaf35 100644
--- a/nixos/tests/resolv.nix
+++ b/nixos/tests/resolv.nix
@@ -1,7 +1,7 @@
 # Test whether DNS resolving returns multiple records and all address families.
 import ./make-test-python.nix ({ pkgs, ... } : {
   name = "resolv";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ckauhaus ];
   };
 
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index dad5bdfff27dc..0cc8bd39afbb0 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -19,7 +19,7 @@ import ./make-test-python.nix (
       {
         name = "restic";
 
-        meta = with pkgs.stdenv.lib.maintainers; {
+        meta = with pkgs.lib.maintainers; {
           maintainers = [ bbigras i077 ];
         };
 
diff --git a/nixos/tests/riak.nix b/nixos/tests/riak.nix
index 6915779e7e9c2..3dd4e333d6691 100644
--- a/nixos/tests/riak.nix
+++ b/nixos/tests/riak.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ lib, pkgs, ... }: {
   name = "riak";
   meta = with lib.maintainers; {
-    maintainers = [ filalex77 ];
+    maintainers = [ Br1ght0ne ];
   };
 
   machine = {
diff --git a/nixos/tests/robustirc-bridge.nix b/nixos/tests/robustirc-bridge.nix
index a5c22d73a34f3..8493fd6282128 100644
--- a/nixos/tests/robustirc-bridge.nix
+++ b/nixos/tests/robustirc-bridge.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "robustirc-bridge";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hax404 ];
   };
 
diff --git a/nixos/tests/roundcube.nix b/nixos/tests/roundcube.nix
index 97e1125694b6b..763f10a7a2dda 100644
--- a/nixos/tests/roundcube.nix
+++ b/nixos/tests/roundcube.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "roundcube";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ globin ];
   };
 
diff --git a/nixos/tests/rspamd.nix b/nixos/tests/rspamd.nix
index bf3f0de620445..7f41e1a795667 100644
--- a/nixos/tests/rspamd.nix
+++ b/nixos/tests/rspamd.nix
@@ -13,10 +13,12 @@ let
     machine.succeed("id rspamd >/dev/null")
   '';
   checkSocket = socket: user: group: mode: ''
-    machine.succeed("ls ${socket} >/dev/null")
-    machine.succeed('[[ "$(stat -c %U ${socket})" == "${user}" ]]')
-    machine.succeed('[[ "$(stat -c %G ${socket})" == "${group}" ]]')
-    machine.succeed('[[ "$(stat -c %a ${socket})" == "${mode}" ]]')
+    machine.succeed(
+        "ls ${socket} >/dev/null",
+        '[[ "$(stat -c %U ${socket})" == "${user}" ]]',
+        '[[ "$(stat -c %G ${socket})" == "${group}" ]]',
+        '[[ "$(stat -c %a ${socket})" == "${mode}" ]]',
+    )
   '';
   simple = name: enableIPv6: makeTest {
     name = "rspamd-${name}";
@@ -54,33 +56,35 @@ in
       services.rspamd = {
         enable = true;
         workers.normal.bindSockets = [{
-          socket = "/run/rspamd.sock";
+          socket = "/run/rspamd/rspamd.sock";
           mode = "0600";
-          owner = "root";
-          group = "root";
+          owner = "rspamd";
+          group = "rspamd";
         }];
         workers.controller.bindSockets = [{
-          socket = "/run/rspamd-worker.sock";
+          socket = "/run/rspamd/rspamd-worker.sock";
           mode = "0666";
-          owner = "root";
-          group = "root";
+          owner = "rspamd";
+          group = "rspamd";
         }];
       };
     };
 
     testScript = ''
       ${initMachine}
-      machine.wait_for_file("/run/rspamd.sock")
-      ${checkSocket "/run/rspamd.sock" "root" "root" "600" }
-      ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" }
+      machine.wait_for_file("/run/rspamd/rspamd.sock")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "600" }
+      ${checkSocket "/run/rspamd/rspamd-worker.sock" "rspamd" "rspamd" "666" }
       machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
       machine.log(
           machine.succeed("grep 'CONFDIR/worker-controller.inc' /etc/rspamd/rspamd.conf")
       )
       machine.log(machine.succeed("grep 'CONFDIR/worker-normal.inc' /etc/rspamd/rspamd.conf"))
-      machine.log(machine.succeed("rspamc -h /run/rspamd-worker.sock stat"))
+      machine.log(machine.succeed("rspamc -h /run/rspamd/rspamd-worker.sock stat"))
       machine.log(
-          machine.succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")
+          machine.succeed(
+              "curl --unix-socket /run/rspamd/rspamd-worker.sock http://localhost/ping"
+          )
       )
     '';
   };
@@ -91,16 +95,16 @@ in
       services.rspamd = {
         enable = true;
         workers.normal.bindSockets = [{
-          socket = "/run/rspamd.sock";
+          socket = "/run/rspamd/rspamd.sock";
           mode = "0600";
-          owner = "root";
-          group = "root";
+          owner = "rspamd";
+          group = "rspamd";
         }];
         workers.controller.bindSockets = [{
-          socket = "/run/rspamd-worker.sock";
+          socket = "/run/rspamd/rspamd-worker.sock";
           mode = "0666";
-          owner = "root";
-          group = "root";
+          owner = "rspamd";
+          group = "rspamd";
         }];
         workers.controller2 = {
           type = "controller";
@@ -116,9 +120,9 @@ in
 
     testScript = ''
       ${initMachine}
-      machine.wait_for_file("/run/rspamd.sock")
-      ${checkSocket "/run/rspamd.sock" "root" "root" "600" }
-      ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" }
+      machine.wait_for_file("/run/rspamd/rspamd.sock")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "600" }
+      ${checkSocket "/run/rspamd/rspamd-worker.sock" "rspamd" "rspamd" "666" }
       machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
       machine.log(
           machine.succeed("grep 'CONFDIR/worker-controller.inc' /etc/rspamd/rspamd.conf")
@@ -137,9 +141,11 @@ in
       machine.wait_until_succeeds(
           "journalctl -u rspamd | grep -i 'starting controller process' >&2"
       )
-      machine.log(machine.succeed("rspamc -h /run/rspamd-worker.sock stat"))
+      machine.log(machine.succeed("rspamc -h /run/rspamd/rspamd-worker.sock stat"))
       machine.log(
-          machine.succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")
+          machine.succeed(
+              "curl --unix-socket /run/rspamd/rspamd-worker.sock http://localhost/ping"
+          )
       )
       machine.log(machine.succeed("curl http://localhost:11335/ping"))
     '';
@@ -209,7 +215,7 @@ in
               return false
             end,
             score = 5.0,
-	          description = 'Allow no cows',
+            description = 'Allow no cows',
             group = "cows",
           }
           rspamd_logger.infox(rspamd_config, 'Work dammit!!!')
diff --git a/nixos/tests/rsyncd.nix b/nixos/tests/rsyncd.nix
index 3639320f645d1..44464e42f28d4 100644
--- a/nixos/tests/rsyncd.nix
+++ b/nixos/tests/rsyncd.nix
@@ -2,24 +2,35 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "rsyncd";
   meta.maintainers = with pkgs.lib.maintainers; [ ehmry ];
 
-  nodes.machine.services.rsyncd = {
-    enable = true;
-    settings = {
-      global = {
-        "reverse lookup" = false;
-        "forward lookup" = false;
+  nodes = let
+    mkNode = socketActivated:
+      { config, ... }: {
+        networking.firewall.allowedTCPPorts = [ config.services.rsyncd.port ];
+        services.rsyncd = {
+          enable = true;
+          inherit socketActivated;
+          settings = {
+            global = {
+              "reverse lookup" = false;
+              "forward lookup" = false;
+            };
+            tmp = {
+              path = "/nix/store";
+              comment = "test module";
+            };
+          };
+        };
       };
-      tmp = {
-        path = "/nix/store";
-        comment = "test module";
-      };
-
-    };
+  in {
+    a = mkNode false;
+    b = mkNode true;
   };
 
   testScript = ''
     start_all()
-    machine.wait_for_unit("rsyncd")
-    machine.succeed("rsync localhost::")
+    a.wait_for_unit("rsync")
+    b.wait_for_unit("sockets.target")
+    b.succeed("rsync a::")
+    a.succeed("rsync b::")
   '';
 })
diff --git a/nixos/tests/rsyslogd.nix b/nixos/tests/rsyslogd.nix
index 50523920c60b2..f35db3bd44b83 100644
--- a/nixos/tests/rsyslogd.nix
+++ b/nixos/tests/rsyslogd.nix
@@ -9,7 +9,7 @@ with pkgs.lib;
 {
   test1 = makeTest {
     name = "rsyslogd-test1";
-    meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
+    meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
     machine = { config, pkgs, ... }: {
       services.rsyslogd.enable = true;
@@ -25,7 +25,7 @@ with pkgs.lib;
 
   test2 = makeTest {
     name = "rsyslogd-test2";
-    meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
+    meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
     machine = { config, pkgs, ... }: {
       services.rsyslogd.enable = true;
diff --git a/nixos/tests/samba-wsdd.nix b/nixos/tests/samba-wsdd.nix
new file mode 100644
index 0000000000000..e7dd17c089a3e
--- /dev/null
+++ b/nixos/tests/samba-wsdd.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "samba-wsdd";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    client_wsdd = { pkgs, ... }: {
+      services.samba-wsdd = {
+        enable = true;
+        interface = "eth1";
+        workgroup = "WORKGROUP";
+        hostname = "CLIENT-WSDD";
+        discovery = true;
+        extraOptions = [ "--no-host" ];
+      };
+      networking.firewall.allowedTCPPorts = [ 5357 ];
+      networking.firewall.allowedUDPPorts = [ 3702 ];
+    };
+
+    server_wsdd = { ... }: {
+      services.samba-wsdd = {
+        enable = true;
+        interface = "eth1";
+        workgroup = "WORKGROUP";
+        hostname = "SERVER-WSDD";
+      };
+      networking.firewall.allowedTCPPorts = [ 5357 ];
+      networking.firewall.allowedUDPPorts = [ 3702 ];
+    };
+  };
+
+  testScript = ''
+    client_wsdd.start()
+    client_wsdd.wait_for_unit("samba-wsdd")
+
+    server_wsdd.start()
+    server_wsdd.wait_for_unit("samba-wsdd")
+
+    client_wsdd.wait_until_succeeds(
+        "echo list | ${pkgs.libressl.nc}/bin/nc -U /run/wsdd/wsdd.sock | grep -i SERVER-WSDD"
+    )
+  '';
+})
diff --git a/nixos/tests/samba.nix b/nixos/tests/samba.nix
index 142269752b34f..d1d50caabfa53 100644
--- a/nixos/tests/samba.nix
+++ b/nixos/tests/samba.nix
@@ -8,7 +8,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   nodes =
     { client =
         { pkgs, ... }:
-        { fileSystems = pkgs.lib.mkVMOverride
+        { virtualisation.fileSystems =
             { "/public" = {
                 fsType = "cifs";
                 device = "//server/public";
diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
index 284b38932cceb..da6d4c9ffe828 100644
--- a/nixos/tests/sanoid.nix
+++ b/nixos/tests/sanoid.nix
@@ -9,7 +9,7 @@ import ./make-test-python.nix ({ pkgs, ... }: let
   };
 in {
   name = "sanoid";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lopsided98 ];
   };
 
@@ -38,7 +38,7 @@ in {
 
       services.syncoid = {
         enable = true;
-        sshKey = "/root/.ssh/id_ecdsa";
+        sshKey = "/var/lib/syncoid/id_ecdsa";
         commonArgs = [ "--no-sync-snap" ];
         commands."pool/test".target = "root@target:pool/test";
       };
@@ -69,11 +69,12 @@ in {
         "udevadm settle",
     )
 
-    source.succeed("mkdir -m 700 /root/.ssh")
     source.succeed(
-        "cat '${snakeOilPrivateKey}' > /root/.ssh/id_ecdsa"
+        "mkdir -m 700 -p /var/lib/syncoid",
+        "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
+        "chmod 600 /var/lib/syncoid/id_ecdsa",
+        "chown -R syncoid:syncoid /var/lib/syncoid/",
     )
-    source.succeed("chmod 600 /root/.ssh/id_ecdsa")
 
     source.succeed("touch /tmp/mnt/test.txt")
     source.systemctl("start --wait sanoid.service")
diff --git a/nixos/tests/sbt-extras.nix b/nixos/tests/sbt-extras.nix
new file mode 100644
index 0000000000000..f1672bf20665c
--- /dev/null
+++ b/nixos/tests/sbt-extras.nix
@@ -0,0 +1,16 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "sbt-extras";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.sbt-extras ];
+    };
+
+  testScript =
+    ''
+      machine.succeed("(sbt -h)")
+    '';
+})
diff --git a/nixos/tests/sbt.nix b/nixos/tests/sbt.nix
new file mode 100644
index 0000000000000..22541232ba65d
--- /dev/null
+++ b/nixos/tests/sbt.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "sbt";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.sbt ];
+    };
+
+  testScript =
+    ''
+      machine.succeed(
+          "(sbt --offline --version 2>&1 || true) | grep 'getting org.scala-sbt sbt ${pkgs.sbt.version}  (this may take some time)'"
+      )
+    '';
+})
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
index f9b961163c3c1..d7c65fa33d67c 100644
--- a/nixos/tests/sddm.nix
+++ b/nixos/tests/sddm.nix
@@ -37,7 +37,7 @@ let
 
     autoLogin = {
       name = "sddm-autologin";
-      meta = with pkgs.stdenv.lib.maintainers; {
+      meta = with pkgs.lib.maintainers; {
         maintainers = [ ttuegel ];
       };
 
diff --git a/nixos/tests/searx.nix b/nixos/tests/searx.nix
new file mode 100644
index 0000000000000..7c28eea30d207
--- /dev/null
+++ b/nixos/tests/searx.nix
@@ -0,0 +1,114 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "searx";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  # basic setup: searx running the built-in webserver
+  nodes.base = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    services.searx = {
+      enable = true;
+      environmentFile = pkgs.writeText "secrets" ''
+        WOLFRAM_API_KEY  = sometoken
+        SEARX_SECRET_KEY = somesecret
+      '';
+
+      settings.server =
+        { port = "8080";
+          bind_address = "0.0.0.0";
+          secret_key = "@SEARX_SECRET_KEY@";
+        };
+      settings.engines = [
+        { name = "wolframalpha";
+          api_key = "@WOLFRAM_API_KEY@";
+          engine = "wolframalpha_api";
+        }
+        { name = "startpage";
+          shortcut = "start";
+        }
+      ];
+    };
+
+  };
+
+  # fancy setup: run in uWSGI and use nginx as proxy
+  nodes.fancy = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    services.searx = {
+      enable = true;
+      # searx refuses to run if unchanged
+      settings.server.secret_key = "somesecret";
+
+      runInUwsgi = true;
+      uwsgiConfig = {
+        # serve using the uwsgi protocol
+        socket = "/run/searx/uwsgi.sock";
+        chmod-socket = "660";
+
+        # use /searx as url "mountpoint"
+        mount = "/searx=searx.webapp:application";
+        module = "";
+        manage-script-name = true;
+      };
+    };
+
+    # use nginx as reverse proxy
+    services.nginx.enable = true;
+    services.nginx.virtualHosts.localhost = {
+      locations."/searx".extraConfig =
+        ''
+          include ${pkgs.nginx}/conf/uwsgi_params;
+          uwsgi_pass unix:/run/searx/uwsgi.sock;
+        '';
+      locations."/searx/static/".alias = "${pkgs.searx}/share/static/";
+    };
+
+    # allow nginx access to the searx socket
+    users.users.nginx.extraGroups = [ "searx" ];
+
+  };
+
+  testScript =
+    ''
+      base.start()
+
+      with subtest("Settings have been merged"):
+          base.wait_for_unit("searx-init")
+          base.wait_for_file("/run/searx/settings.yml")
+          output = base.succeed(
+              "${pkgs.yq-go}/bin/yq eval"
+              " '.engines[] | select(.name==\"startpage\") | .shortcut'"
+              " /run/searx/settings.yml"
+          ).strip()
+          assert output == "start", "Settings not merged"
+
+      with subtest("Environment variables have been substituted"):
+          base.succeed("grep -q somesecret /run/searx/settings.yml")
+          base.succeed("grep -q sometoken /run/searx/settings.yml")
+          base.copy_from_vm("/run/searx/settings.yml")
+
+      with subtest("Basic setup is working"):
+          base.wait_for_open_port(8080)
+          base.wait_for_unit("searx")
+          base.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost:8080"
+          )
+          base.shutdown()
+
+      with subtest("Nginx+uWSGI setup is working"):
+          fancy.start()
+          fancy.wait_for_open_port(80)
+          fancy.wait_for_unit("uwsgi")
+          fancy.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost/searx >&2"
+          )
+          fancy.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/js/bootstrap.min.js >&2"
+          )
+    '';
+})
diff --git a/nixos/tests/service-runner.nix b/nixos/tests/service-runner.nix
index 39ae66fe1116e..58f46735f56dc 100644
--- a/nixos/tests/service-runner.nix
+++ b/nixos/tests/service-runner.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "service-runner";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ roberth ];
   };
 
@@ -29,7 +29,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             """
         )
         machine.wait_for_open_port(80)
-        machine.succeed(f"curl {url}")
+        machine.succeed(f"curl -f {url}")
         machine.succeed("kill -INT $(cat my-nginx.pid)")
         machine.wait_for_closed_port(80)
   '';
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
new file mode 100644
index 0000000000000..e5755e8e0878c
--- /dev/null
+++ b/nixos/tests/shadow.nix
@@ -0,0 +1,116 @@
+let
+  password1 = "foobar";
+  password2 = "helloworld";
+  password3 = "bazqux";
+  password4 = "asdf123";
+in import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "shadow";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.shadow = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.shadow ];
+
+    users = {
+      mutableUsers = true;
+      users.emma = {
+        password = password1;
+        shell = pkgs.bash;
+      };
+      users.layla = {
+        password = password2;
+        shell = pkgs.shadow;
+      };
+      users.ash = {
+        password = password4;
+        shell = pkgs.bash;
+      };
+    };
+  };
+
+  testScript = ''
+    shadow.wait_for_unit("multi-user.target")
+    shadow.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+    with subtest("Normal login"):
+        shadow.send_key("alt-f2")
+        shadow.wait_until_succeeds(f"[ $(fgconsole) = 2 ]")
+        shadow.wait_for_unit(f"getty@tty2.service")
+        shadow.wait_until_succeeds(f"pgrep -f 'agetty.*tty2'")
+        shadow.wait_until_tty_matches(2, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(2, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.send_chars("whoami > /tmp/1\n")
+        shadow.wait_for_file("/tmp/1")
+        assert "emma" in shadow.succeed("cat /tmp/1")
+
+    with subtest("Switch user"):
+        shadow.send_chars("su - ash\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password4}\n")
+        shadow.sleep(2)
+        shadow.send_chars("whoami > /tmp/3\n")
+        shadow.wait_for_file("/tmp/3")
+        assert "ash" in shadow.succeed("cat /tmp/3")
+
+    with subtest("Change password"):
+        shadow.send_key("alt-f3")
+        shadow.wait_until_succeeds(f"[ $(fgconsole) = 3 ]")
+        shadow.wait_for_unit(f"getty@tty3.service")
+        shadow.wait_until_succeeds(f"pgrep -f 'agetty.*tty3'")
+        shadow.wait_until_tty_matches(3, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(3, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.send_chars("passwd\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.sleep(2)
+        shadow.send_key("alt-f4")
+        shadow.wait_until_succeeds(f"[ $(fgconsole) = 4 ]")
+        shadow.wait_for_unit(f"getty@tty4.service")
+        shadow.wait_until_succeeds(f"pgrep -f 'agetty.*tty4'")
+        shadow.wait_until_tty_matches(4, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(4, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.wait_until_tty_matches(4, "Login incorrect")
+        shadow.wait_until_tty_matches(4, "login:")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(4, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.send_chars("whoami > /tmp/2\n")
+        shadow.wait_for_file("/tmp/2")
+        assert "emma" in shadow.succeed("cat /tmp/2")
+
+    with subtest("Groups"):
+        assert "foobar" not in shadow.succeed("groups emma")
+        shadow.succeed("groupadd foobar")
+        shadow.succeed("usermod -a -G foobar emma")
+        assert "foobar" in shadow.succeed("groups emma")
+
+    with subtest("nologin shell"):
+        shadow.send_key("alt-f5")
+        shadow.wait_until_succeeds(f"[ $(fgconsole) = 5 ]")
+        shadow.wait_for_unit(f"getty@tty5.service")
+        shadow.wait_until_succeeds(f"pgrep -f 'agetty.*tty5'")
+        shadow.wait_until_tty_matches(5, "login: ")
+        shadow.send_chars("layla\n")
+        shadow.wait_until_tty_matches(5, "login: layla")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.send_chars("${password2}\n")
+        shadow.wait_until_tty_matches(5, "login:")
+  '';
+})
diff --git a/nixos/tests/signal-desktop.nix b/nixos/tests/signal-desktop.nix
index 65ae49a267d98..c424288e00a90 100644
--- a/nixos/tests/signal-desktop.nix
+++ b/nixos/tests/signal-desktop.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
 {
   name = "signal-desktop";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ flokli ];
   };
 
diff --git a/nixos/tests/simple.nix b/nixos/tests/simple.nix
index 3810a2cd3a582..b4d90f750ecfb 100644
--- a/nixos/tests/simple.nix
+++ b/nixos/tests/simple.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "simple";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/slurm.nix b/nixos/tests/slurm.nix
index a54c5d9db4826..3702d243b486b 100644
--- a/nixos/tests/slurm.nix
+++ b/nixos/tests/slurm.nix
@@ -86,14 +86,16 @@ in {
 
     dbd =
       { pkgs, ... } :
-      {
+      let
+        passFile = pkgs.writeText "dbdpassword" "password123";
+      in {
         networking.firewall.enable = false;
         systemd.tmpfiles.rules = [
           "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
         ];
         services.slurm.dbdserver = {
           enable = true;
-          storagePass = "password123";
+          storagePassFile = "${passFile}";
         };
         services.mysql = {
           enable = true;
@@ -107,12 +109,12 @@ in {
             ensurePermissions = { "slurm_acct_db.*" = "ALL PRIVILEGES"; };
             name = "slurm";
           }];
-          extraOptions = ''
+          settings.mysqld = {
             # recommendations from: https://slurm.schedmd.com/accounting.html#mysql-configuration
-            innodb_buffer_pool_size=1024M
-            innodb_log_file_size=64M
-            innodb_lock_wait_timeout=900
-          '';
+            innodb_buffer_pool_size="1024M";
+            innodb_log_file_size="64M";
+            innodb_lock_wait_timeout=900;
+          };
         };
       };
 
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
index 4f8f0fcc9fe2e..ccacf60cfe4bd 100644
--- a/nixos/tests/smokeping.nix
+++ b/nixos/tests/smokeping.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "smokeping";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ cransom ];
   };
 
@@ -8,6 +8,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     sm =
       { ... }:
       {
+        networking.domain = "example.com"; # FQDN: sm.example.com
         services.smokeping = {
           enable = true;
           port = 8081;
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
index 92534f1028190..2fef636251401 100644
--- a/nixos/tests/snapcast.nix
+++ b/nixos/tests/snapcast.nix
@@ -4,9 +4,11 @@ let
   port = 10004;
   tcpPort = 10005;
   httpPort = 10080;
+  tcpStreamPort = 10006;
+  bufferSize = 742;
 in {
   name = "snapcast";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hexa ];
   };
 
@@ -17,18 +19,27 @@ in {
         port = port;
         tcp.port = tcpPort;
         http.port = httpPort;
+        buffer = bufferSize;
         streams = {
           mpd = {
             type = "pipe";
             location = "/run/snapserver/mpd";
+            query.mode = "create";
           };
           bluetooth = {
             type = "pipe";
             location = "/run/snapserver/bluetooth";
           };
+          tcp = {
+            type = "tcp";
+            location = "127.0.0.1:${toString tcpStreamPort}";
+          };
         };
       };
     };
+    client = {
+      environment.systemPackages = [ pkgs.snapcast ];
+    };
   };
 
   testScript = ''
@@ -42,6 +53,7 @@ in {
     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}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString tcpStreamPort}")
 
     with subtest("check that pipes are created"):
         server.succeed("test -p /run/snapserver/mpd")
@@ -54,5 +66,12 @@ in {
         server.succeed(
             "curl --fail http://localhost:${toString httpPort}/jsonrpc -d '{json.dumps(get_rpc_version)}'"
         )
+
+    with subtest("test a connection"):
+        client.execute("systemd-run snapclient -h server -p ${toString port}")
+        server.wait_until_succeeds(
+            "journalctl -o cat -u snapserver.service | grep -q 'Hello from'"
+        )
+        client.wait_until_succeeds("journalctl -o cat -u run-\* | grep -q ${toString bufferSize}")
   '';
 })
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
index 018102d7f6409..098d8d9d72f55 100644
--- a/nixos/tests/snapper.nix
+++ b/nixos/tests/snapper.nix
@@ -9,7 +9,7 @@ import ./make-test-python.nix ({ ... }:
 
     virtualisation.emptyDiskImages = [ 4096 ];
 
-    fileSystems = lib.mkVMOverride {
+    virtualisation.fileSystems = {
       "/home" = {
         device = "/dev/disk/by-label/aux";
         fsType = "btrfs";
diff --git a/nixos/tests/sogo.nix b/nixos/tests/sogo.nix
index 016331a9eed68..3f600b4cd5553 100644
--- a/nixos/tests/sogo.nix
+++ b/nixos/tests/sogo.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "sogo";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ajs124 das_j ];
   };
 
diff --git a/nixos/tests/solr.nix b/nixos/tests/solr.nix
index dc5770e16bc72..86efe87c70783 100644
--- a/nixos/tests/solr.nix
+++ b/nixos/tests/solr.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "solr";
-  meta.maintainers = [ pkgs.stdenv.lib.maintainers.aanderse ];
+  meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
   machine =
     { config, pkgs, ... }:
diff --git a/nixos/tests/spacecookie.nix b/nixos/tests/spacecookie.nix
index 6eff32a2e75d7..5b5022a742787 100644
--- a/nixos/tests/spacecookie.nix
+++ b/nixos/tests/spacecookie.nix
@@ -32,7 +32,7 @@ in
       ${gopherHost}.wait_for_unit("spacecookie.service")
       client.wait_for_unit("network.target")
 
-      fileResponse = client.succeed("curl -s gopher://${gopherHost}//${fileName}")
+      fileResponse = client.succeed("curl -f -s gopher://${gopherHost}//${fileName}")
 
       # the file response should return our created file exactly
       if not (fileResponse == "${fileContent}\n"):
@@ -41,7 +41,7 @@ in
       # sanity check on the directory listing: we serve a directory and a file
       # via gopher, so the directory listing should have exactly two entries,
       # one with gopher file type 0 (file) and one with file type 1 (directory).
-      dirResponse = client.succeed("curl -s gopher://${gopherHost}")
+      dirResponse = client.succeed("curl -f -s gopher://${gopherHost}")
       dirEntries = [l[0] for l in dirResponse.split("\n") if len(l) > 0]
       dirEntries.sort()
 
diff --git a/nixos/tests/spike.nix b/nixos/tests/spike.nix
index 47763e75ffa2b..cb89df73877b3 100644
--- a/nixos/tests/spike.nix
+++ b/nixos/tests/spike.nix
@@ -1,11 +1,11 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 
 let
-  riscvPkgs = import ../.. { crossSystem = pkgs.stdenv.lib.systems.examples.riscv64-embedded; };
+  riscvPkgs = import ../.. { crossSystem = pkgs.lib.systems.examples.riscv64-embedded; };
 in
 {
   name = "spike";
-  meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ blitz ]; };
+  meta = with pkgs.lib.maintainers; { maintainers = [ blitz ]; };
 
   machine = { pkgs, lib, ... }: {
     environment.systemPackages = [ pkgs.spike riscvPkgs.riscv-pk riscvPkgs.hello ];
diff --git a/nixos/tests/sslh.nix b/nixos/tests/sslh.nix
index 2a800aa52d0aa..17094606e8e6b 100644
--- a/nixos/tests/sslh.nix
+++ b/nixos/tests/sslh.nix
@@ -78,6 +78,6 @@ import ./make-test-python.nix {
         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"
+        assert client.succeed(f"curl -f {arg} http://server:443").strip() == "hello world"
   '';
 }
diff --git a/nixos/tests/sssd-ldap.nix b/nixos/tests/sssd-ldap.nix
index b68403a0102a8..e3119348eac7e 100644
--- a/nixos/tests/sssd-ldap.nix
+++ b/nixos/tests/sssd-ldap.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+({ pkgs, ... }:
   let
     dbDomain = "example.org";
     dbSuffix = "dc=example,dc=org";
@@ -7,45 +7,63 @@ import ./make-test-python.nix ({ pkgs, ... }:
     ldapRootPassword = "foobar";
 
     testUser = "alice";
-  in
-  {
+  in import ./make-test-python.nix {
     name = "sssd-ldap";
 
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ bbigras ];
     };
 
     machine = { pkgs, ... }: {
       services.openldap = {
         enable = true;
-        rootdn = "cn=${ldapRootUser},${dbSuffix}";
-        rootpw = ldapRootPassword;
-        suffix = dbSuffix;
-        declarativeContents = ''
-          dn: ${dbSuffix}
-          objectClass: top
-          objectClass: dcObject
-          objectClass: organization
-          o: ${dbDomain}
+        settings = {
+          children = {
+            "cn=schema".includes = [
+              "${pkgs.openldap}/etc/schema/core.ldif"
+              "${pkgs.openldap}/etc/schema/cosine.ldif"
+              "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              "${pkgs.openldap}/etc/schema/nis.ldif"
+            ];
+            "olcDatabase={1}mdb" = {
+              attrs = {
+                objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                olcDatabase = "{1}mdb";
+                olcDbDirectory = "/var/db/openldap";
+                olcSuffix = dbSuffix;
+                olcRootDN = "cn=${ldapRootUser},${dbSuffix}";
+                olcRootPW = ldapRootPassword;
+              };
+            };
+          };
+        };
+        declarativeContents = {
+          ${dbSuffix} = ''
+            dn: ${dbSuffix}
+            objectClass: top
+            objectClass: dcObject
+            objectClass: organization
+            o: ${dbDomain}
 
-          dn: ou=posix,${dbSuffix}
-          objectClass: top
-          objectClass: organizationalUnit
+            dn: ou=posix,${dbSuffix}
+            objectClass: top
+            objectClass: organizationalUnit
 
-          dn: ou=accounts,ou=posix,${dbSuffix}
-          objectClass: top
-          objectClass: organizationalUnit
+            dn: ou=accounts,ou=posix,${dbSuffix}
+            objectClass: top
+            objectClass: organizationalUnit
 
-          dn: uid=${testUser},ou=accounts,ou=posix,${dbSuffix}
-          objectClass: person
-          objectClass: posixAccount
-          # userPassword: somePasswordHash
-          homeDirectory: /home/${testUser}
-          uidNumber: 1234
-          gidNumber: 1234
-          cn: ""
-          sn: ""
-        '';
+            dn: uid=${testUser},ou=accounts,ou=posix,${dbSuffix}
+            objectClass: person
+            objectClass: posixAccount
+            # userPassword: somePasswordHash
+            homeDirectory: /home/${testUser}
+            uidNumber: 1234
+            gidNumber: 1234
+            cn: ""
+            sn: ""
+          '';
+        };
       };
 
       services.sssd = {
diff --git a/nixos/tests/sssd.nix b/nixos/tests/sssd.nix
index 4c6ca86c74c8f..5c1abdca6aef4 100644
--- a/nixos/tests/sssd.nix
+++ b/nixos/tests/sssd.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "sssd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bbigras ];
   };
   machine = { pkgs, ... }: {
diff --git a/nixos/tests/strongswan-swanctl.nix b/nixos/tests/strongswan-swanctl.nix
index 152c0d61c5434..0cf181ee62a56 100644
--- a/nixos/tests/strongswan-swanctl.nix
+++ b/nixos/tests/strongswan-swanctl.nix
@@ -31,7 +31,7 @@ let
   proposals     = [ "aes128-sha256-x25519" ];
 in {
   name = "strongswan-swanctl";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ basvandijk ];
+  meta.maintainers = with pkgs.lib.maintainers; [ basvandijk ];
   nodes = {
 
     alice = { ... } : {
diff --git a/nixos/tests/sudo.nix b/nixos/tests/sudo.nix
index 8c38f1b47ef0c..2a85c490665a9 100644
--- a/nixos/tests/sudo.nix
+++ b/nixos/tests/sudo.nix
@@ -6,7 +6,7 @@ let
 in
   import ./make-test-python.nix ({ pkgs, ...} : {
     name = "sudo";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ lschuermann ];
     };
 
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 9ef96cec5ef36..78adf7ffa7da5 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "switch-test";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ gleber ];
   };
 
diff --git a/nixos/tests/sympa.nix b/nixos/tests/sympa.nix
index 280691f7cb402..eb38df180a789 100644
--- a/nixos/tests/sympa.nix
+++ b/nixos/tests/sympa.nix
@@ -30,7 +30,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     machine.wait_for_unit("sympa.service")
     machine.wait_for_unit("wwsympa.service")
     assert "Mailing lists service" in machine.succeed(
-        "curl --insecure -L http://localhost/"
+        "curl --fail --insecure -L http://localhost/"
     )
   '';
 })
diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix
index 0a01da52b68bc..4581e3fd4fbe5 100644
--- a/nixos/tests/syncthing-init.nix
+++ b/nixos/tests/syncthing-init.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: let
 
 in {
   name = "syncthing-init";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ lassulus ];
+  meta.maintainers = with pkgs.lib.maintainers; [ lassulus ];
 
   machine = {
     services.syncthing = {
diff --git a/nixos/tests/syncthing-relay.nix b/nixos/tests/syncthing-relay.nix
index cd72ef1cbe1de..a0233c969ec06 100644
--- a/nixos/tests/syncthing-relay.nix
+++ b/nixos/tests/syncthing-relay.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ lib, pkgs, ... }: {
   name = "syncthing-relay";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ delroth ];
+  meta.maintainers = with pkgs.lib.maintainers; [ delroth ];
 
   machine = {
     environment.systemPackages = [ pkgs.jq ];
@@ -19,7 +19,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     machine.wait_for_open_port(12346)
 
     out = machine.succeed(
-        "curl -sS http://localhost:12346/status | jq -r '.options.\"provided-by\"'"
+        "curl -sSf http://localhost:12346/status | jq -r '.options.\"provided-by\"'"
     )
     assert "nixos-test" in out
   '';
diff --git a/nixos/tests/syncthing.nix b/nixos/tests/syncthing.nix
index 9e2a8e01e3fb8..5536b7055cc9c 100644
--- a/nixos/tests/syncthing.nix
+++ b/nixos/tests/syncthing.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ lib, pkgs, ... }: {
   name = "syncthing";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ chkno ];
+  meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
 
   nodes = rec {
     a = {
@@ -25,7 +25,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
             "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
+            "curl -Ssf -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})
@@ -39,7 +39,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
         )
         newConf = json.dumps(conf)
         host.succeed(
-            "curl -Ss -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config -d %s"
+            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/system/config -d %s"
             % (APIKey, shlex.quote(newConf))
         )
 
diff --git a/nixos/tests/systemd-analyze.nix b/nixos/tests/systemd-analyze.nix
index a78ba08cd55cb..186f5aee7b85e 100644
--- a/nixos/tests/systemd-analyze.nix
+++ b/nixos/tests/systemd-analyze.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
 
 {
   name = "systemd-analyze";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ raskin ];
   };
 
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
index 7a663dd9b4284..3c93cb82d646d 100644
--- a/nixos/tests/systemd-boot.nix
+++ b/nixos/tests/systemd-boot.nix
@@ -18,7 +18,7 @@ in
 {
   basic = makeTest {
     name = "systemd-boot";
-    meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
     machine = common;
 
@@ -42,7 +42,7 @@ in
   # 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 ];
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
     machine = { pkgs, lib, ... }: {
       imports = [ common ];
@@ -68,7 +68,7 @@ in
 
   update = makeTest {
     name = "systemd-boot-update";
-    meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ];
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
     machine = common;
 
diff --git a/nixos/tests/systemd-journal.nix b/nixos/tests/systemd-journal.nix
new file mode 100644
index 0000000000000..6ab7c72463181
--- /dev/null
+++ b/nixos/tests/systemd-journal.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "systemd-journal";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lewo ];
+  };
+
+  machine = { pkgs, lib, ... }: {
+    services.journald.enableHttpGateway = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    machine.succeed("journalctl --grep=systemd")
+
+    machine.succeed(
+        "${pkgs.curl}/bin/curl -s localhost:19531/machine | ${pkgs.jq}/bin/jq -e '.hostname == \"machine\"'"
+    )
+  '';
+})
diff --git a/nixos/tests/systemd-networkd-dhcpserver.nix b/nixos/tests/systemd-networkd-dhcpserver.nix
index f1a2662f8cb4b..b52c1499718b7 100644
--- a/nixos/tests/systemd-networkd-dhcpserver.nix
+++ b/nixos/tests/systemd-networkd-dhcpserver.nix
@@ -3,7 +3,7 @@
 # reachable via the DHCP allocated address.
 import ./make-test-python.nix ({pkgs, ...}: {
   name = "systemd-networkd-dhcpserver";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ tomfitzhenry ];
   };
   nodes = {
diff --git a/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
index 99395fe3023f3..bce78f09fdcce 100644
--- a/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
+++ b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
@@ -9,7 +9,7 @@
 
 import ./make-test-python.nix ({pkgs, ...}: {
   name = "systemd-networkd-ipv6-prefix-delegation";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ andir ];
   };
   nodes = {
diff --git a/nixos/tests/systemd-networkd-vrf.nix b/nixos/tests/systemd-networkd-vrf.nix
index bd4751f8e4359..9f09d801f77ac 100644
--- a/nixos/tests/systemd-networkd-vrf.nix
+++ b/nixos/tests/systemd-networkd-vrf.nix
@@ -38,14 +38,14 @@ in {
           matchConfig.Name = "vrf1";
           networkConfig.IPForward = "yes";
           routes = [
-            { routeConfig = { Destination = "192.168.1.2"; Metric = "100"; }; }
+            { routeConfig = { Destination = "192.168.1.2"; Metric = 100; }; }
           ];
         };
         networks."10-vrf2" = {
           matchConfig.Name = "vrf2";
           networkConfig.IPForward = "yes";
           routes = [
-            { routeConfig = { Destination = "192.168.2.3"; Metric = "100"; }; }
+            { routeConfig = { Destination = "192.168.2.3"; Metric = 100; }; }
           ];
         };
 
diff --git a/nixos/tests/systemd-networkd.nix b/nixos/tests/systemd-networkd.nix
index d5fb2431dbad5..4f2cb75f5a0cb 100644
--- a/nixos/tests/systemd-networkd.nix
+++ b/nixos/tests/systemd-networkd.nix
@@ -61,7 +61,7 @@ let generateNodeConf = { lib, pkgs, config, privk, pubk, peerId, nodeId, ...}: {
     };
 in import ./make-test-python.nix ({pkgs, ... }: {
   name = "networkd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ninjatrappeur ];
   };
   nodes = {
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
index dfa16eecfad21..e0685f53a9454 100644
--- a/nixos/tests/systemd.nix
+++ b/nixos/tests/systemd.nix
@@ -9,7 +9,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
     environment.systemPackages = [ pkgs.cryptsetup ];
 
-    fileSystems = lib.mkVMOverride {
+    virtualisation.fileSystems = {
       "/test-x-initrd-mount" = {
         device = "/dev/vdb";
         fsType = "ext2";
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
     systemd.shutdown.test = pkgs.writeScript "test.shutdown" ''
       #!${pkgs.runtimeShell}
-      PATH=${lib.makeBinPath (with pkgs; [ utillinux coreutils ])}
+      PATH=${lib.makeBinPath (with pkgs; [ util-linux coreutils ])}
       mount -t 9p shared -o trans=virtio,version=9p2000.L /tmp/shared
       touch /tmp/shared/shutdown-test
       umount /tmp/shared
@@ -82,6 +82,10 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "systemd-run --pty --property=Type=oneshot --property=DynamicUser=yes --property=User=iamatest whoami"
         )
 
+    with subtest("regression test for https://bugs.freedesktop.org/show_bug.cgi?id=77507"):
+        retcode, output = machine.execute("systemctl status testservice1.service")
+        assert retcode in [0, 3]  # https://bugs.freedesktop.org/show_bug.cgi?id=77507
+
     # Regression test for https://github.com/NixOS/nixpkgs/issues/35268
     with subtest("file system with x-initrd.mount is not unmounted"):
         machine.succeed("mountpoint -q /test-x-initrd-mount")
@@ -122,17 +126,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         machine.wait_for_unit("multi-user.target")
         assert "fq_codel" in machine.succeed("sysctl net.core.default_qdisc")
 
-    # Test cgroup accounting is enabled
-    with subtest("systemd cgroup accounting is enabled"):
-        machine.wait_for_unit("multi-user.target")
-        assert "yes" in machine.succeed(
-            "systemctl show testservice1.service -p IOAccounting"
-        )
-
-        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")
@@ -168,5 +161,25 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         machine.succeed("systemctl status systemd-cryptsetup@luks1.service")
         machine.succeed("mkdir -p /tmp/luks1")
         machine.succeed("mount /dev/mapper/luks1 /tmp/luks1")
+
+    # Do some IP traffic
+    output_ping = machine.succeed(
+        "systemd-run --wait -- /run/wrappers/bin/ping -c 1 127.0.0.1 2>&1"
+    )
+
+    with subtest("systemd reports accounting data on system.slice"):
+        output = machine.succeed("systemctl status system.slice")
+        assert "CPU:" in output
+        assert "Memory:" in output
+
+        assert "IP:" in output
+        assert "0B in, 0B out" not in output
+
+        assert "IO:" in output
+        assert "0B read, 0B written" not in output
+
+    with subtest("systemd per-unit accounting works"):
+        assert "IP traffic received: 84B" in output_ping
+        assert "IP traffic sent: 84B" in output_ping
   '';
 })
diff --git a/nixos/tests/teeworlds.nix b/nixos/tests/teeworlds.nix
index edf588968788c..17e9eeb869b02 100644
--- a/nixos/tests/teeworlds.nix
+++ b/nixos/tests/teeworlds.nix
@@ -10,7 +10,7 @@ let
 
 in {
   name = "teeworlds";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ hax404 ];
   };
 
diff --git a/nixos/tests/telegraf.nix b/nixos/tests/telegraf.nix
index 73f741b113574..d99680ce2c3c4 100644
--- a/nixos/tests/telegraf.nix
+++ b/nixos/tests/telegraf.nix
@@ -1,17 +1,20 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "telegraf";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mic92 ];
   };
 
   machine = { ... }: {
     services.telegraf.enable = true;
+    services.telegraf.environmentFiles = [(pkgs.writeText "secrets" ''
+      SECRET=example
+    '')];
     services.telegraf.extraConfig = {
       agent.interval = "1s";
       agent.flush_interval = "1s";
       inputs.exec = {
         commands = [
-          "${pkgs.runtimeShell} -c 'echo example,tag=a i=42i'"
+          "${pkgs.runtimeShell} -c 'echo $SECRET,tag=a i=42i'"
         ];
         timeout = "5s";
         data_format = "influx";
diff --git a/nixos/tests/tinc/default.nix b/nixos/tests/tinc/default.nix
new file mode 100644
index 0000000000000..31b675ad35c06
--- /dev/null
+++ b/nixos/tests/tinc/default.nix
@@ -0,0 +1,139 @@
+import ../make-test-python.nix ({ lib, ... }:
+  let
+    snakeoil-keys = import ./snakeoil-keys.nix;
+
+    hosts = lib.attrNames snakeoil-keys;
+
+    subnetOf = name: config:
+      let
+        subnets = config.services.tinc.networks.myNetwork.hostSettings.${name}.subnets;
+      in
+      (builtins.head subnets).address;
+
+    makeTincHost = name: { subnet, extraConfig ? { } }: lib.mkMerge [
+      {
+        subnets = [{ address = subnet; }];
+        settings = {
+          Ed25519PublicKey = snakeoil-keys.${name}.ed25519Public;
+        };
+        rsaPublicKey = snakeoil-keys.${name}.rsaPublic;
+      }
+      extraConfig
+    ];
+
+    makeTincNode = { config, ... }: name: extraConfig: lib.mkMerge [
+      {
+        services.tinc.networks.myNetwork = {
+          inherit name;
+          rsaPrivateKeyFile =
+            builtins.toFile "rsa.priv" snakeoil-keys.${name}.rsaPrivate;
+          ed25519PrivateKeyFile =
+            builtins.toFile "ed25519.priv" snakeoil-keys.${name}.ed25519Private;
+
+          hostSettings = lib.mapAttrs makeTincHost {
+            static = {
+              subnet = "10.0.0.11";
+              # Only specify the addresses in the node's vlans, Tinc does not
+              # seem to try each one, unlike the documentation suggests...
+              extraConfig.addresses = map
+                (vlan: { address = "192.168.${toString vlan}.11"; port = 655; })
+                config.virtualisation.vlans;
+            };
+            dynamic1 = { subnet = "10.0.0.21"; };
+            dynamic2 = { subnet = "10.0.0.22"; };
+          };
+        };
+
+        networking.useDHCP = false;
+
+        networking.interfaces."tinc.myNetwork" = {
+          virtual = true;
+          virtualType = "tun";
+          ipv4.addresses = [{
+            address = subnetOf name config;
+            prefixLength = 24;
+          }];
+        };
+
+        # Prevents race condition between NixOS service and tinc creating the
+        # interface.
+        # See: https://github.com/NixOS/nixpkgs/issues/27070
+        systemd.services."tinc.myNetwork" = {
+          after = [ "network-addresses-tinc.myNetwork.service" ];
+          requires = [ "network-addresses-tinc.myNetwork.service" ];
+        };
+
+        networking.firewall.allowedTCPPorts = [ 655 ];
+        networking.firewall.allowedUDPPorts = [ 655 ];
+      }
+      extraConfig
+    ];
+
+  in
+  {
+    name = "tinc";
+    meta.maintainers = with lib.maintainers; [ minijackson ];
+
+    nodes = {
+
+      static = { ... } @ args:
+        makeTincNode args "static" {
+          virtualisation.vlans = [ 1 2 ];
+
+          networking.interfaces.eth1.ipv4.addresses = [{
+            address = "192.168.1.11";
+            prefixLength = 24;
+          }];
+
+          networking.interfaces.eth2.ipv4.addresses = [{
+            address = "192.168.2.11";
+            prefixLength = 24;
+          }];
+        };
+
+
+      dynamic1 = { ... } @ args:
+        makeTincNode args "dynamic1" {
+          virtualisation.vlans = [ 1 ];
+        };
+
+      dynamic2 = { ... } @ args:
+        makeTincNode args "dynamic2" {
+          virtualisation.vlans = [ 2 ];
+        };
+
+    };
+
+    testScript = ''
+      start_all()
+
+      static.wait_for_unit("tinc.myNetwork.service")
+      dynamic1.wait_for_unit("tinc.myNetwork.service")
+      dynamic2.wait_for_unit("tinc.myNetwork.service")
+
+      # Static is accessible by the other hosts
+      dynamic1.succeed("ping -c5 192.168.1.11")
+      dynamic2.succeed("ping -c5 192.168.2.11")
+
+      # The other hosts are in separate vlans
+      dynamic1.fail("ping -c5 192.168.2.11")
+      dynamic2.fail("ping -c5 192.168.1.11")
+
+      # Each host can ping themselves through Tinc
+      static.succeed("ping -c5 10.0.0.11")
+      dynamic1.succeed("ping -c5 10.0.0.21")
+      dynamic2.succeed("ping -c5 10.0.0.22")
+
+      # Static is accessible by the other hosts through Tinc
+      dynamic1.succeed("ping -c5 10.0.0.11")
+      dynamic2.succeed("ping -c5 10.0.0.11")
+
+      # Static can access the other hosts through Tinc
+      static.succeed("ping -c5 10.0.0.21")
+      static.succeed("ping -c5 10.0.0.22")
+
+      # The other hosts in separate vlans can access each other through Tinc
+      dynamic1.succeed("ping -c5 10.0.0.22")
+      dynamic2.succeed("ping -c5 10.0.0.21")
+    '';
+  })
diff --git a/nixos/tests/tinc/snakeoil-keys.nix b/nixos/tests/tinc/snakeoil-keys.nix
new file mode 100644
index 0000000000000..650e57d61d4a7
--- /dev/null
+++ b/nixos/tests/tinc/snakeoil-keys.nix
@@ -0,0 +1,157 @@
+{
+  static = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      IPR+ur5LfVdm6VlR1+FGIkbkL8Enkb9sejBa/JP6tXkg/vHoraIp70srb6jAUFm5
+      3YbCJiBjLW3dy16qM5PovBoWtr5hoqYYA9dFLOys8FBUFFsIGfKhnbk7g25iwxbO
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "AqV7aeIqKGGQfXxijMLfRAVRBLixnS45G5OoduIc8mD";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEAxDHl0TIhhT2yH5rT+Q7MLnj+Ir8bbs3uaPqnzcxWzN1EfVP8
+      TWt5fSTrF2Dc78Kyu5ZNALrp7tUj0GZAegp1YeYJ28p3qTwCveywtCwbB4dI987S
+      yJwq95kE9aoyLa+cT99VwSTdb2YowQv2tWj/idxE3oJ+qZjy9tE5mysXm7jmTQDx
+      +U0XmNe6MHjKXc01Ener41u0ykJLeUfdgJ1zEyM2rQGtaHpIXfMT6kmxCaMcAMLg
+      YFpI38/1pQGQtROKdGOaUomx2m058bkMsJhTiBjESiLRDElRGxmMJ732crGJP0GR
+      ChJkaX/CnxHq7R0daZfwoTVHRu6N7WDbFQL5twIDAQABAoIBAQCM/fLTIHyYXRr5
+      vXFhxXGUYBz56W6UdWdEiAU5TwR92vFSQ53IIVlARtyvg0ui/b8mMcAKq0hb+03u
+      gN0LFyL+BKvHCLxvoRGzXTorcJrIET+t3jL6OchjANNgnDvNOytQ9wWQdKaxXLAi
+      8y8LdXZWozXW1d6ikKjiGL+WNCSWIcq83ktSJZcohihptU9Un16FYQzdolSC8RtI
+      XyT7i1ye6hW/wJTJxqZ4taX3EPat85kXS234VGSqg9bb2A1yE+U8Rq37bf8AKldJ
+      NUQB3JyxnkYGJcqvzDmz139+744VWxDRvXDA5vU29LC6f8bGBvwEttD98QW+pgmB
+      1NBU1Uo5AoGBAOzUk6k74h1RarwXaftjh/9Pures0CfNNnrkJApzFCh4bAoHNxq6
+      SSXqLcc/vvX2+YaZ72nn5YTo+JLQP6evM9oUaqRMAxa3nzoNCtF8U2r48UWmoUQE
+      aZCYbD3m7IVWFacCKRVaVTMZMTTicypSnXcbCSIEH8PRs9+L4jkHgql9AoGBANQT
+      TZECVhIaQnyRiKWlUE8G1QKzXIxjmfyirBe+ftlIG2XMXasAtQ4VRxpnorgqUnIH
+      BVrIbvRx21zlqwZbrZvyb1jHWRoyi1cqBPijpYBUm5LbV2jgHPhnfhRVqdD4CDKj
+      NQzIQrNymFaMWAoOQv/DE3g+Txr0fm9Ztu8ZRXZDAoGAHh3SQT0aPfwyhIS9t3gq
+      vS7YYa8aMVWJTgthAessbxERPB06xq1Vy/qBo8rZb9HeXV2J8n/I0iQGKDVPQvWm
+      tF7QSOBZrDPhjbJG4+jZesr5c5ADBfFBs1+OtDh/b11JF5nQu6RnHT5g4YbCemlT
+      GOhZOvgnSfGK3CyfsfzggskCgYEAmpKDK5kPUNxw70hH16v5L9Bj+zbt0qlZ+Ag8
+      9IV1ATuMNJNTBitay6v4iidVM3QtaUzyuytxq5s87qW7FMRHcm2ueH+70ttaMiq/
+      OtZT74g7aDuUpy0KEIemHn4dauENYJMSPIHOE+sHW7WpCZNBhBcUHsUTdSsU6GX0
+      bqr1tO8CgYBpZdR2OoX/rn8nwjmtBOH38aPnCpaAfdI2Eq2Lg6DjksP6TBt53a+R
+      m1lk6Kt37BPPZQ85SBr7ywvDgUzfoD7uSmHujF2JUHPsdrg9nx7pNIGlW6DlS9OU
+      oNXGAJ/6/y6F8uDbToUfrwFq5tKMypEEa32kFtxb9f0XQ5fSgHrBEw==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEAxDHl0TIhhT2yH5rT+Q7MLnj+Ir8bbs3uaPqnzcxWzN1EfVP8TWt5
+      fSTrF2Dc78Kyu5ZNALrp7tUj0GZAegp1YeYJ28p3qTwCveywtCwbB4dI987SyJwq
+      95kE9aoyLa+cT99VwSTdb2YowQv2tWj/idxE3oJ+qZjy9tE5mysXm7jmTQDx+U0X
+      mNe6MHjKXc01Ener41u0ykJLeUfdgJ1zEyM2rQGtaHpIXfMT6kmxCaMcAMLgYFpI
+      38/1pQGQtROKdGOaUomx2m058bkMsJhTiBjESiLRDElRGxmMJ732crGJP0GRChJk
+      aX/CnxHq7R0daZfwoTVHRu6N7WDbFQL5twIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+
+  dynamic1 = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      wHNC2IMXfYtL4ehdsCX154HBvlIZYEiTOnXtckWMUtEAiX9fu7peyBkp9q+yOy9c
+      xsNyssLL78lt0GoweCxlu3Sza2oBQAcwb+6tuv7P/bqzcG005uCwquyCz8LVymXA
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "t0smNaAEAH8mver77+z/m6MnBNdurAsqrswM/Sls5FA";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEApukYNGFNWvVlmx75LyOE7MEcd/ViV+yEyk+4cIBXYJ3Ouw+/
+      oEuh8ghQfsiUtbUPR6hPYhX2ZV8XGhuU2nAXVQV0sfZ8pdkbHQ6wHUqFcUIQAVvS
+      Wpm2DvZM8jkbCPP64/x5nukPwQ8VoNnb62rWGzbcj7rOeb7ndMK0TpX5Wwv8F297
+      nKTNCEDbK3DLTj3VD+QGnw6AoEt5i44vViAWZBXuHLHWTDC0Nq8GG+9TKODkEwt5
+      4dgN2X9f+WTVAYhZT3SayHLqIFIMQunN89RpWwhHSW+JIRfAfuT1TbP+wA5ptDeI
+      ktCkJwWyv4hK6l800BJ9GW1nbId5LPa58ipaVwIDAQABAoIBAHcw3WgKVAMwWm57
+      n9ZZtwKapInFYYUIEYungj5UaBFGn+pVRLJjUDJWXaUr94YK1e6F8qpIpLufPBAY
+      wiN7CC5exwaOzlRgxUvqwTkpjkFiu6s8tuqb+baVjD0tKnEqSW+lS/R+2hEzhG5p
+      JPLoSB0HAFpjPC8UdJSctcWos3if3mvOGkGCKyTkrwaJgECDfD+lZ+NBIAiYLSps
+      jWLE+XlY1+nfPdLUQ+TRSv3IikJ/CWbvJLl9EE1tKhkY564KytwZrkIdJlc7NyRO
+      HpzhyMzHu1GLsr+OsBZByNNUxEPU+bzkDQluRXUSIUs9zZoBiCQr3o04qGPTEX9n
+      pNU60gECgYEA3Uf+c80eqzjDxv+O0YzC+9x6A+yMrV56siGkKRPMlrSqjX7iE2Yg
+      tUjD25kEvtaFuB3f/7zp3h4O/VLZgXreRtXHvdrfoyyJGHvHIyCGm8sw8CEWsKo4
+      1LgZUzdPJRkXJq1zOgS0r1xsA1UDC4s02Ww2HwNeVWtmLUyCpA+B/ccCgYEAwRk9
+      tbe82eq1a85zZiPVXP2qvDH5+Vz9YiMky8xsBnoxmz2siR+NdvWBLcE2VDIY8MK1
+      9a1dz2a7cAHQBrtWtACFVY4zvr69DumApjbQRClDYpJ42tp2VbzlMcUDIoKudRQV
+      CObhrE4w4yfVizXFyH9+4Tsg5NzVYuGg9fUJ/vECgYEAoRz7KouNqfMhsLF/5hkM
+      Gt9zw4mm/9ALm8kcwn/U9WHD0FQy/Rbd98BsQmaOavi80cqGvqhoyz2tgkqhbUHt
+      tzuOPDCxphgWFcqBupTDDYoLLruYzraRvGfyoIFj0coL7jBZ9kNY31l2l5J9LhmE
+      OE4utbP5Kk6RTagocpWL+x8CgYB48CwcIcWf3kZeDOFtuUeqhB1o3Qwox7rSuhwT
+      oCaQL/vdtNTY1PAu7zhGxdoXBYFlWS3JfxlgCoGedyQo8zAscJ8RpIx4DNIwAsLW
+      V0I9TnKry/zxZR30OOh7MV7zQFGvdjJubtwspJQt0QcHt1f2aRO4UOYbMMxcr9+1
+      7BCkoQKBgQDBEtg1hx9zYGg1WN2TBSvh6NShi9S23r6IZ3Up8vz6Z2rcwB3UuhKi
+      xluI2ZFwM9s+7UOpaGC+hnc1aMHDEguYOPXoIzvebbYAdN4AkrsJ5d0r1GoEe64E
+      UXxrfuv5LeJ/vkUgWof+U3/jGOVvrjzi5y1xOC0r3kiSpMa85s1dhQ==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEApukYNGFNWvVlmx75LyOE7MEcd/ViV+yEyk+4cIBXYJ3Ouw+/oEuh
+      8ghQfsiUtbUPR6hPYhX2ZV8XGhuU2nAXVQV0sfZ8pdkbHQ6wHUqFcUIQAVvSWpm2
+      DvZM8jkbCPP64/x5nukPwQ8VoNnb62rWGzbcj7rOeb7ndMK0TpX5Wwv8F297nKTN
+      CEDbK3DLTj3VD+QGnw6AoEt5i44vViAWZBXuHLHWTDC0Nq8GG+9TKODkEwt54dgN
+      2X9f+WTVAYhZT3SayHLqIFIMQunN89RpWwhHSW+JIRfAfuT1TbP+wA5ptDeIktCk
+      JwWyv4hK6l800BJ9GW1nbId5LPa58ipaVwIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+
+  dynamic2 = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      oUx9JdIstZLMj3ZPD8mP3ITsUscCTIXhNF3VKFUVi/ma5uk50/1vrEohfDraiMxj
+      gAWthpkhnFzUbp+YlOHE7/Z3h1a/br2/tk8DoZ5PV6ufoV1MaBlGdu+TZgeZou0t
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "f2dYt2/2q9fLJ/AaW+Tlu7HaVNjWQpRnr/UGoXGqLdL";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEAtQfijPX3BwOAs2Y0EuNjcBmsI90uYqNAonrFgTtcVwERIVE6
+      p6alSEakazhByujBg3jI8oPKC8eO0IJ7x/BWcgxqaw8hsPfJZFnRlwEcU5kK4c+j
+      UNS+hJOXp0x97T1edLpSFHDK9bZ2necblHKG5MsI4UsxEa+CZ0yoIybwWCDmYuya
+      PvE7CeNNa+CIOUbtPVoN4p/aBj0vZeerNBBuodNkglKRxj4l9wD9uOx4S9sdK5lu
+      q/rkxlViBoXRAshT+G2d/u/7/WPoiKB3QJcF33z8UfrlsTRnDDqOMSGisTPSv2LK
+      4QLN4hWOGXAYQqZcxTkvvjl62mCDuoy0TM+CKQIDAQABAoIBAFKpMAxXf52nPswr
+      /dkmFVCpmE2kADsv+iJ21tpkpYxgw1aoRZUp5cyz3P3MaVZio4IJ1A/Ql6B7Vb3l
+      5ulr170p6CnMdgDdlAsLbEV8T1foyOxFKHiPPBNDZXsR1WpPnGLGdRY6TqKV12HQ
+      lmpZRTkRcJOXBufhcTUD7r5mWFaUoZ7so6VxR4L4Tzcgv1Rl4S6jgnHOQdO6lj47
+      BaPjpBb+hplJ4wsRm91dQ7JApYq25XZwyxnBwQ2zAwb46wsuFxDPHlSc4wU7qTt6
+      x2omm33Xy2cm8L1XQhrassZzldSnAyaLBh9DC3+vFPLODDxdz5M2kpHujYYctRhv
+      CICMYJUCgYEA7mWVYuw0S8FNjaLx6n9Q1hr9d9vAFDd3NEaegH586xvhYNxf6n+C
+      2zZloVLEsX0UnBU/6ZtLAUfxUIqlvDS2r1VjSYG5SNxM6/vyGl17Niu1jC8nzf7M
+      V1WtDCHhT4ikZCuNkAldtgI7CXVdCVO/fTqVhjk4hDblJo7VsCZSZysCgYEAwmXp
+      TwlDHapDqA8UxClZuxS8k+2hthny3ihRPCuT34yqAz074zYG97ZBKwIa4Lm1vnkc
+      mwU7yR2aK7IYeU4ScfWm1mLjkW5iaNV/sG7iTz/RP4mBAs3KSGmuhhz8sFWcXByU
+      IZyvMJvC+FpgJQJn/Xc8ZmdImvXlZd6k8v4/kfsCgYEA6VzFPB2OH63slb4w42SX
+      o86t2dtiDigxZxnN5GhtLdSP7borpigF10JLf/y+kCOpvhRLCQk8Bdf/z+C41iAf
+      yEhktbrnvfvwzHxHhSmHCAMHZ19trodCTiePCrZLkQhoK6o6nAmfEyDh26NoXE3/
+      v71OSyLOQRZfgDwHz7PjrBsCgYAe0zojpjxWP+FqjLmmQUhROgCNFGlIDuVMBOic
+      uexAznVG/ja42KBSNzwuLa9FYy1Gfr3idvn78g24UA1BbvfNyj4iUJv1O6OvK+uL
+      dom8N0pe4NbsMuWYhel+qqoG7AxXLtDuY4IEGy7XYr1MIQ2MS5PwSQBiUguGE7/k
+      KBy8cQKBgQCyC9R8VWJxQLqJxZGa9Ful01bSuntB5OLRfEjFCCuGiY/3Vj+mCiQL
+      GOfMOi2jrcnSNgUm0uevmiFCq9m7QiPiAcSYKXPWhsz/55jJIGcZy8bwyhZ2s2Mg
+      BGeZgj4RFORidqkt5g/KJz0+Wp6Ks4sLoCvOzkpeXvLzFVyzGkihrw==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEAtQfijPX3BwOAs2Y0EuNjcBmsI90uYqNAonrFgTtcVwERIVE6p6al
+      SEakazhByujBg3jI8oPKC8eO0IJ7x/BWcgxqaw8hsPfJZFnRlwEcU5kK4c+jUNS+
+      hJOXp0x97T1edLpSFHDK9bZ2necblHKG5MsI4UsxEa+CZ0yoIybwWCDmYuyaPvE7
+      CeNNa+CIOUbtPVoN4p/aBj0vZeerNBBuodNkglKRxj4l9wD9uOx4S9sdK5luq/rk
+      xlViBoXRAshT+G2d/u/7/WPoiKB3QJcF33z8UfrlsTRnDDqOMSGisTPSv2LK4QLN
+      4hWOGXAYQqZcxTkvvjl62mCDuoy0TM+CKQIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+}
diff --git a/nixos/tests/tor.nix b/nixos/tests/tor.nix
index ad07231557c3c..c061f59226cfb 100644
--- a/nixos/tests/tor.nix
+++ b/nixos/tests/tor.nix
@@ -17,7 +17,7 @@ rec {
       environment.systemPackages = with pkgs; [ netcat ];
       services.tor.enable = true;
       services.tor.client.enable = true;
-      services.tor.controlPort = 9051;
+      services.tor.settings.ControlPort = 9051;
     };
 
   testScript = ''
diff --git a/nixos/tests/trac.nix b/nixos/tests/trac.nix
index 7953f8d41f771..d6914c1008175 100644
--- a/nixos/tests/trac.nix
+++ b/nixos/tests/trac.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trac";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
@@ -14,6 +14,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     start_all()
     machine.wait_for_unit("trac.service")
     machine.wait_for_open_port(8000)
-    machine.wait_until_succeeds("curl -L http://localhost:8000/ | grep 'Trac Powered'")
+    machine.wait_until_succeeds("curl -fL http://localhost:8000/ | grep 'Trac Powered'")
   '';
 })
diff --git a/nixos/tests/traefik.nix b/nixos/tests/traefik.nix
index 0e21a7cf8437f..4eeae29acadf3 100644
--- a/nixos/tests/traefik.nix
+++ b/nixos/tests/traefik.nix
@@ -2,7 +2,7 @@
 # and a Docker container.
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "traefik";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ joko ];
   };
 
diff --git a/nixos/tests/transmission.nix b/nixos/tests/transmission.nix
index 37c0352dcfb8f..7e2648804de21 100644
--- a/nixos/tests/transmission.nix
+++ b/nixos/tests/transmission.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "transmission";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ coconnor ];
   };
 
diff --git a/nixos/tests/trezord.nix b/nixos/tests/trezord.nix
index b7b3dd31942bb..fb60cb4aff100 100644
--- a/nixos/tests/trezord.nix
+++ b/nixos/tests/trezord.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trezord";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ mmahut _1000101 ];
   };
   nodes = {
@@ -14,6 +14,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     start_all()
     machine.wait_for_unit("trezord.service")
     machine.wait_for_open_port(21325)
-    machine.wait_until_succeeds("curl -L http://localhost:21325/status/ | grep Version")
+    machine.wait_until_succeeds("curl -fL http://localhost:21325/status/ | grep Version")
   '';
 })
diff --git a/nixos/tests/trickster.nix b/nixos/tests/trickster.nix
index 713ac8f0b2fae..acb2e735c39f8 100644
--- a/nixos/tests/trickster.nix
+++ b/nixos/tests/trickster.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trickster";
-  meta = with pkgs.stdenv.lib; {
+  meta = with pkgs.lib; {
     maintainers = with maintainers; [ _1000101 ];
   };
 
@@ -19,19 +19,19 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     prometheus.wait_for_unit("prometheus.service")
     prometheus.wait_for_open_port(9090)
     prometheus.wait_until_succeeds(
-        "curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+        "curl -fL http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
     )
     trickster.wait_for_unit("trickster.service")
     trickster.wait_for_open_port(8082)
     trickster.wait_for_open_port(9090)
     trickster.wait_until_succeeds(
-        "curl -L http://localhost:8082/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+        "curl -fL http://localhost:8082/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
     )
     trickster.wait_until_succeeds(
-        "curl -L http://prometheus:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+        "curl -fL http://prometheus:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
     )
     trickster.wait_until_succeeds(
-        "curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+        "curl -fL http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
     )
   '';
 })
diff --git a/nixos/tests/tuptime.nix b/nixos/tests/tuptime.nix
index 36ce2b1ae1923..6d37e30698390 100644
--- a/nixos/tests/tuptime.nix
+++ b/nixos/tests/tuptime.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "tuptime";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ evils ];
   };
 
diff --git a/nixos/tests/ucg.nix b/nixos/tests/ucg.nix
new file mode 100644
index 0000000000000..7769fd01fce42
--- /dev/null
+++ b/nixos/tests/ucg.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "ucg";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ AndersonTorres ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.ucg ];
+  };
+
+  testScript = ''
+    machine.succeed("echo 'Lorem ipsum dolor sit amet\n2.7182818284590' > /tmp/foo")
+    assert "dolor" in machine.succeed("ucg 'dolor' /tmp/foo")
+    assert "Lorem" in machine.succeed("ucg --ignore-case 'lorem' /tmp/foo")
+    machine.fail("ucg --word-regexp '2718' /tmp/foo")
+    machine.fail("ucg 'pisum' /tmp/foo")
+  '';
+})
diff --git a/nixos/tests/udisks2.nix b/nixos/tests/udisks2.nix
index 50a0239689181..1f01cc6de4d6f 100644
--- a/nixos/tests/udisks2.nix
+++ b/nixos/tests/udisks2.nix
@@ -11,7 +11,7 @@ in
 
 {
   name = "udisks2";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
diff --git a/nixos/tests/unbound.nix b/nixos/tests/unbound.nix
new file mode 100644
index 0000000000000..c88231636226b
--- /dev/null
+++ b/nixos/tests/unbound.nix
@@ -0,0 +1,278 @@
+/*
+ Test that our unbound module indeed works as most users would expect.
+ There are a few settings that we must consider when modifying the test. The
+ ususal use-cases for unbound are
+   * running a recursive DNS resolver on the local machine
+   * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via UDP/53 & TCP/53
+   * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via TCP/853 (DoT)
+   * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/53 & UDP/53
+   * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/853 (DoT)
+
+ In the below test setup we are trying to implement all of those use cases.
+
+ Another aspect that we cover is access to the local control UNIX socket. It
+ can optionally be enabled and users can optionally be in a group to gain
+ access. Users that are not in the group (except for root) should not have
+ access to that socket. Also, when there is no socket configured, users
+ shouldn't be able to access the control socket at all. Not even root.
+*/
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    # common client configuration that we can just use for the multitude of
+    # clients we are constructing
+    common = { lib, pkgs, ... }: {
+      config = {
+        environment.systemPackages = [ pkgs.knot-dns ];
+
+        # disable the root anchor update as we do not have internet access during
+        # the test execution
+        services.unbound.enableRootTrustAnchor = false;
+      };
+    };
+
+    cert = pkgs.runCommandNoCC "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+      openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=dns.example.local'
+      mkdir -p $out
+      cp key.pem cert.pem $out
+    '';
+  in
+  {
+    name = "unbound";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ andir ];
+    };
+
+    nodes = {
+
+      # The server that actually serves our zones, this tests unbounds authoriative mode
+      authoritative = { lib, pkgs, config, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.1"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::1"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [ 53 ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          interfaces = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ];
+          allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ];
+          extraConfig = ''
+            server:
+              local-data: "example.local. IN A 1.2.3.4"
+              local-data: "example.local. IN AAAA abcd::eeff"
+          '';
+        };
+      };
+
+      # The resolver that knows that fowards (only) to the authoritative server
+      # and listens on UDP/53, TCP/53 & TCP/853.
+      resolver = { lib, nodes, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.2"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::2"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [
+          53 # regular DNS
+          853 # DNS over TLS
+        ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ];
+          interfaces = [ "::1" "127.0.0.1" "192.168.0.2" "fd21::2" "192.168.0.2@853" "fd21::2@853" "::1@853" "127.0.0.1@853" ];
+          forwardAddresses = [
+            (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address
+            (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address
+          ];
+          extraConfig = ''
+            server:
+              tls-service-pem: ${cert}/cert.pem
+              tls-service-key: ${cert}/key.pem
+          '';
+        };
+      };
+
+      # machine that runs a local unbound that will be reconfigured during test execution
+      local_resolver = { lib, nodes, config, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.3"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::3"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [
+          53 # regular DNS
+        ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          allowedAccess = [ "::1" "127.0.0.0/8" ];
+          interfaces = [ "::1" "127.0.0.1" ];
+          localControlSocketPath = "/run/unbound/unbound.ctl";
+          extraConfig = ''
+            include: "/etc/unbound/extra*.conf"
+          '';
+        };
+
+        users.users = {
+          # user that is permitted to access the unix socket
+          someuser.extraGroups = [
+            config.users.users.unbound.group
+          ];
+
+          # user that is not permitted to access the unix socket
+          unauthorizeduser = {};
+        };
+
+        environment.etc = {
+          "unbound-extra1.conf".text = ''
+            forward-zone:
+              name: "example.local."
+              forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address}
+              forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}
+          '';
+          "unbound-extra2.conf".text = ''
+            auth-zone:
+              name: something.local.
+              zonefile: ${pkgs.writeText "zone" ''
+                something.local. IN A 3.4.5.6
+              ''}
+          '';
+        };
+      };
+
+
+      # plain node that only has network access and doesn't run any part of the
+      # resolver software locally
+      client = { lib, nodes, ... }: {
+        imports = [ common ];
+        networking.nameservers = [
+          (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address
+          (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address
+        ];
+        networking.interfaces.eth1.ipv4.addresses = [
+          { address = "192.168.0.10"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = [
+          { address = "fd21::10"; prefixLength = 64; }
+        ];
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      import typing
+      import json
+
+      zone = "example.local."
+      records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")]
+
+
+      def query(
+          machine,
+          host: str,
+          query_type: str,
+          query: str,
+          expected: typing.Optional[str] = None,
+          args: typing.Optional[typing.List[str]] = None,
+      ):
+          """
+          Execute a single query and compare the result with expectation
+          """
+          text_args = ""
+          if args:
+              text_args = " ".join(args)
+
+          out = machine.succeed(
+              f"kdig {text_args} {query} {query_type} @{host} +short"
+          ).strip()
+          machine.log(f"{host} replied with {out}")
+          if expected:
+              assert expected == out, f"Expected `{expected}` but got `{out}`"
+
+
+      def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]):
+          """
+          Run queries for the given remotes on the given machine.
+          """
+          for query_type, expected in records:
+              for remote in remotes:
+                  query(machine, remote, query_type, zone, expected, args)
+                  query(machine, remote, query_type, zone, expected, ["+tcp"] + args)
+                  if doh:
+                      query(
+                          machine,
+                          remote,
+                          query_type,
+                          zone,
+                          expected,
+                          ["+tcp", "+tls"] + args,
+                      )
+
+
+      client.start()
+      authoritative.wait_for_unit("unbound.service")
+
+      # verify that we can resolve locally
+      with subtest("test the authoritative servers local responses"):
+          test(authoritative, ["::1", "127.0.0.1"])
+
+      resolver.wait_for_unit("unbound.service")
+
+      with subtest("root is unable to use unbounc-control when the socket is not configured"):
+          resolver.succeed("which unbound-control")  # the binary must exist
+          resolver.fail("unbound-control list_forwards")  # the invocation must fail
+
+      # verify that the resolver is able to resolve on all the local protocols
+      with subtest("test that the resolver resolves on all protocols and transports"):
+          test(resolver, ["::1", "127.0.0.1"], doh=True)
+
+      resolver.wait_for_unit("multi-user.target")
+
+      with subtest("client should be able to query the resolver"):
+          test(client, ["${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address}", "${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}"], doh=True)
+
+      # discard the client we do not need anymore
+      client.shutdown()
+
+      local_resolver.wait_for_unit("multi-user.target")
+
+      # link a new config file to /etc/unbound/extra.conf
+      local_resolver.succeed("ln -s /etc/unbound-extra1.conf /etc/unbound/extra1.conf")
+
+      # reload the server & ensure the forwarding works
+      with subtest("test that the local resolver resolves on all protocols and transports"):
+          local_resolver.succeed("systemctl reload unbound")
+          print(local_resolver.succeed("journalctl -u unbound -n 1000"))
+          test(local_resolver, ["::1", "127.0.0.1"], args=["+timeout=60"])
+
+      with subtest("test that we can use the unbound control socket"):
+          out = local_resolver.succeed(
+              "sudo -u someuser -- unbound-control list_forwards"
+          ).strip()
+
+          # Thank you black! Can't really break this line into a readable version.
+          expected = "example.local. IN forward ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address} ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}"
+          assert out == expected, f"Expected `{expected}` but got `{out}` instead."
+          local_resolver.fail("sudo -u unauthorizeduser -- unbound-control list_forwards")
+
+
+      # link a new config file to /etc/unbound/extra.conf
+      local_resolver.succeed("ln -sf /etc/unbound-extra2.conf /etc/unbound/extra2.conf")
+
+      # reload the server & ensure the new local zone works
+      with subtest("test that we can query the new local zone"):
+          local_resolver.succeed("unbound-control reload")
+          r = [("A", "3.4.5.6")]
+          test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r)
+    '';
+  })
diff --git a/nixos/tests/upnp.nix b/nixos/tests/upnp.nix
index a7d837ea0708e..451c8607d0ebd 100644
--- a/nixos/tests/upnp.nix
+++ b/nixos/tests/upnp.nix
@@ -15,7 +15,7 @@ let
 in
 {
   name = "upnp";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ bobvanderlinden ];
   };
 
@@ -90,7 +90,7 @@ in
       client1.succeed("upnpc -a ${internalClient1Address} 9000 9000 TCP")
 
       client1.wait_for_unit("httpd")
-      client2.wait_until_succeeds("curl http://${externalRouterAddress}:9000/")
+      client2.wait_until_succeeds("curl -f http://${externalRouterAddress}:9000/")
     '';
 
 })
diff --git a/nixos/tests/usbguard.nix b/nixos/tests/usbguard.nix
new file mode 100644
index 0000000000000..cba905db44f34
--- /dev/null
+++ b/nixos/tests/usbguard.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "usbguard";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tnias ];
+  };
+
+  machine =
+    { ... }:
+    {
+      services.usbguard = {
+        enable = true;
+        IPCAllowedUsers = [ "alice" "root" ];
+
+        # As virtual USB devices get attached to the "QEMU USB Hub" we need to
+        # allow Hubs. Otherwise we would have to explicitly allow them too.
+        rules = ''
+          allow with-interface equals { 09:00:00 }
+        '';
+      };
+      imports = [ ./common/user-account.nix ];
+    };
+
+  testScript = ''
+    # create a blank disk image for our fake USB stick
+    with open(machine.state_dir + "/usbstick.img", "wb") as stick:
+        stick.write(b"\x00" * (1024 * 1024))
+
+    # wait for machine to have started and the usbguard service to be up
+    machine.wait_for_unit("usbguard.service")
+
+    with subtest("IPC access control"):
+        # User "alice" is allowed to access the IPC interface
+        machine.succeed("su alice -c 'usbguard list-devices'")
+
+        # User "bob" is not allowed to access the IPC interface
+        machine.fail("su bob -c 'usbguard list-devices'")
+
+    with subtest("check basic functionality"):
+        # at this point we expect that no USB HDD is connected
+        machine.fail("usbguard list-devices | grep -E 'QEMU USB HARDDRIVE'")
+
+        # insert usb device
+        machine.send_monitor_command(
+            f"drive_add 0 id=stick,if=none,file={stick.name},format=raw"
+        )
+        machine.send_monitor_command("device_add usb-storage,id=stick,drive=stick")
+
+        # the attached USB HDD should show up after a short while
+        machine.wait_until_succeeds("usbguard list-devices | grep -E 'QEMU USB HARDDRIVE'")
+
+        # at this point there should be a **blocked** USB HDD
+        machine.succeed("usbguard list-devices | grep -E 'block.*QEMU USB HARDDRIVE'")
+        machine.fail("usbguard list-devices | grep -E ' allow .*QEMU USB HARDDRIVE'")
+
+        # allow storage devices
+        machine.succeed("usbguard allow-device 'with-interface { 08:*:* }'")
+
+        # at this point there should be an **allowed** USB HDD
+        machine.succeed("usbguard list-devices | grep -E ' allow .*QEMU USB HARDDRIVE'")
+        machine.fail("usbguard list-devices | grep -E ' block .*QEMU USB HARDDRIVE'")
+  '';
+})
diff --git a/nixos/tests/uwsgi.nix b/nixos/tests/uwsgi.nix
index 78a87147f55cd..80dcde324aad7 100644
--- a/nixos/tests/uwsgi.nix
+++ b/nixos/tests/uwsgi.nix
@@ -1,29 +1,58 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "uwsgi";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 ];
   };
+
   machine = { pkgs, ... }: {
-    services.uwsgi.enable = true;
-    services.uwsgi.plugins = [ "python3" ];
-    services.uwsgi.instance = {
-      type = "emperor";
-      vassals.hello = {
+    users.users.hello  =
+      { isSystemUser = true;
+        group = "hello";
+      };
+    users.groups.hello = { };
+
+    services.uwsgi = {
+      enable = true;
+      plugins = [ "python3" "php" ];
+      capabilities = [ "CAP_NET_BIND_SERVICE" ];
+      instance.type = "emperor";
+
+      instance.vassals.hello = {
         type = "normal";
-        master = true;
-        workers = 2;
-        http = ":8000";
+        immediate-uid = "hello";
+        immediate-gid = "hello";
         module = "wsgi:application";
+        http = ":80";
+        cap = "net_bind_service";
+        pythonPackages = self: [ self.flask ];
         chdir = pkgs.writeTextDir "wsgi.py" ''
           from flask import Flask
+          import subprocess
           application = Flask(__name__)
 
           @application.route("/")
           def hello():
-              return "Hello World!"
+              return "Hello, World!"
+
+          @application.route("/whoami")
+          def whoami():
+              whoami = "${pkgs.coreutils}/bin/whoami"
+              proc = subprocess.run(whoami, capture_output=True)
+              return proc.stdout.decode().strip()
+        '';
+      };
+
+      instance.vassals.php = {
+        type = "normal";
+        master = true;
+        workers = 2;
+        http-socket = ":8000";
+        http-socket-modifier1 = 14;
+        php-index = "index.php";
+        php-docroot = pkgs.writeTextDir "index.php" ''
+          <?php echo "Hello World\n"; ?>
         '';
-        pythonPackages = self: with self; [ flask ];
       };
     };
   };
@@ -32,7 +61,21 @@ import ./make-test-python.nix ({ pkgs, ... }:
     ''
       machine.wait_for_unit("multi-user.target")
       machine.wait_for_unit("uwsgi.service")
-      machine.wait_for_open_port(8000)
-      assert "Hello World" in machine.succeed("curl -v 127.0.0.1:8000")
+
+      with subtest("uWSGI has started"):
+          machine.wait_for_unit("uwsgi.service")
+
+      with subtest("Vassal can bind on port <1024"):
+          machine.wait_for_open_port(80)
+          hello = machine.succeed("curl -f http://machine").strip()
+          assert "Hello, World!" in hello, f"Excepted 'Hello, World!', got '{hello}'"
+
+      with subtest("Vassal is running as dedicated user"):
+          username = machine.succeed("curl -f http://machine/whoami").strip()
+          assert username == "hello", f"Excepted 'hello', got '{username}'"
+
+      with subtest("PHP plugin is working"):
+          machine.wait_for_open_port(8000)
+          assert "Hello World" in machine.succeed("curl -fv http://machine:8000")
     '';
 })
diff --git a/nixos/tests/vault-postgresql.nix b/nixos/tests/vault-postgresql.nix
new file mode 100644
index 0000000000000..a563aead22a3b
--- /dev/null
+++ b/nixos/tests/vault-postgresql.nix
@@ -0,0 +1,70 @@
+/* This test checks that
+    - multiple config files can be loaded
+    - the storage backend can be in a file outside the nix store
+      as is required for security (required because while confidentiality is
+      always covered, availability isn't)
+    - the postgres integration works
+ */
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "vault-postgresql";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 roberth ];
+  };
+  machine = { lib, pkgs, ... }: {
+    virtualisation.memorySize = 512;
+    environment.systemPackages = [ pkgs.vault ];
+    environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
+    services.vault.enable = true;
+    services.vault.extraSettingsPaths = [ "/run/vault.hcl" ];
+
+    systemd.services.vault = {
+      after = [
+        "postgresql.service"
+      ];
+      # Try for about 10 minutes rather than the default of 5 attempts.
+      serviceConfig.RestartSec = 1;
+      serviceConfig.StartLimitBurst = 600;
+    };
+    # systemd.services.vault.unitConfig.RequiresMountsFor = "/run/keys/";
+
+    services.postgresql.enable = true;
+    services.postgresql.initialScript = pkgs.writeText "init.psql" ''
+      CREATE USER vaultuser WITH ENCRYPTED PASSWORD 'thisisthepass';
+      GRANT CONNECT ON DATABASE postgres TO vaultuser;
+
+      -- https://www.vaultproject.io/docs/configuration/storage/postgresql
+      CREATE TABLE vault_kv_store (
+        parent_path TEXT COLLATE "C" NOT NULL,
+        path        TEXT COLLATE "C",
+        key         TEXT COLLATE "C",
+        value       BYTEA,
+        CONSTRAINT pkey PRIMARY KEY (path, key)
+      );
+      CREATE INDEX parent_path_idx ON vault_kv_store (parent_path);
+
+      GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO vaultuser;
+    '';
+  };
+
+  testScript =
+    ''
+      secretConfig = """
+          storage "postgresql" {
+            connection_url = "postgres://vaultuser:thisisthepass@localhost/postgres?sslmode=disable"
+          }
+          """
+
+      start_all()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("cat >/root/vault.hcl <<EOF\n%s\nEOF\n" % secretConfig)
+      machine.succeed(
+          "install --owner vault --mode 0400 /root/vault.hcl /run/vault.hcl; rm /root/vault.hcl"
+      )
+      machine.wait_for_unit("vault.service")
+      machine.wait_for_open_port(8200)
+      machine.succeed("vault operator init")
+      machine.succeed("vault status | grep Sealed | grep true")
+    '';
+})
diff --git a/nixos/tests/vault.nix b/nixos/tests/vault.nix
index ac8cf0703da5d..59bccbe259590 100644
--- a/nixos/tests/vault.nix
+++ b/nixos/tests/vault.nix
@@ -1,13 +1,14 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "vault";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 ];
   };
   machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.vault ];
     environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
     services.vault.enable = true;
+    virtualisation.memorySize = 512;
   };
 
   testScript =
diff --git a/nixos/tests/vector.nix b/nixos/tests/vector.nix
new file mode 100644
index 0000000000000..583e60ddc5681
--- /dev/null
+++ b/nixos/tests/vector.nix
@@ -0,0 +1,37 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../.. { inherit system config; } }:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+{
+  test1 = makeTest {
+    name = "vector-test1";
+    meta.maintainers = [ pkgs.lib.maintainers.happysalada ];
+
+    machine = { config, pkgs, ... }: {
+      services.vector = {
+        enable = true;
+        journaldAccess = true;
+        settings = {
+          sources.journald.type = "journald";
+
+          sinks = {
+            file = {
+              type = "file";
+              inputs = [ "journald" ];
+              path = "/var/lib/vector/logs.log";
+              encoding = { codec = "ndjson"; };
+            };
+          };
+        };
+      };
+    };
+
+    # ensure vector is forwarding the messages appropriately
+    testScript = ''
+      machine.wait_for_unit("vector.service")
+      machine.succeed("test -f /var/lib/vector/logs.log")
+    '';
+  };
+}
diff --git a/nixos/tests/victoriametrics.nix b/nixos/tests/victoriametrics.nix
index 73ef8b7286153..5e364b67bf87e 100644
--- a/nixos/tests/victoriametrics.nix
+++ b/nixos/tests/victoriametrics.nix
@@ -2,7 +2,7 @@
 
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "victoriametrics";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ yorickvp ];
   };
 
@@ -19,9 +19,11 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     # write some points and run simple query
     out = one.succeed(
-        "curl -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'"
+        "curl -f -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'"
+    )
+    cmd = (
+        """curl -f -s -G 'http://localhost:8428/api/v1/export' -d 'match={__name__!=""}'"""
     )
-    cmd = """curl -s -G 'http://localhost:8428/api/v1/export' -d 'match={__name__!=""}'"""
     # data takes a while to appear
     one.wait_until_succeeds(f"[[ $({cmd} | wc -l) -ne 0 ]]")
     out = one.succeed(cmd)
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 0d9eafa4a20f3..0a7369b0fa2aa 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -24,7 +24,7 @@ let
 
     miniInit = ''
       #!${pkgs.runtimeShell} -xe
-      export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.utillinux ]}"
+      export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}"
 
       mkdir -p /run/dbus
       cat > /etc/passwd <<EOF
@@ -72,7 +72,7 @@ let
 
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
-      copy_bin_and_libs "${pkgs.utillinux}/bin/unshare"
+      copy_bin_and_libs "${pkgs.util-linux}/bin/unshare"
       ${(attrs.extraUtilsCommands or (const "")) pkgs}
     '';
 
@@ -122,7 +122,7 @@ let
         "$diskImage" "$out/disk.vdi"
     '';
 
-    buildInputs = [ pkgs.utillinux pkgs.perl ];
+    buildInputs = [ pkgs.util-linux pkgs.perl ];
   } ''
     ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
     ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
@@ -402,7 +402,7 @@ let
       # (keep black happy)
     '';
 
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ aszlig cdepillabout ];
     };
   };
diff --git a/nixos/tests/vscodium.nix b/nixos/tests/vscodium.nix
new file mode 100644
index 0000000000000..ca75da35b1e19
--- /dev/null
+++ b/nixos/tests/vscodium.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "vscodium";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ turion ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    virtualisation.memorySize = 2047;
+    services.xserver.enable = true;
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = with pkgs; [
+      vscodium
+    ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    # Start up X
+    start_all()
+    machine.wait_for_x()
+
+    # Start VSCodium with a file that doesn't exist yet
+    machine.fail("ls /home/alice/foo.txt")
+    machine.succeed("su - alice -c 'codium foo.txt' &")
+
+    # Wait for the window to appear
+    machine.wait_for_text("VSCodium")
+
+    # Save file
+    machine.send_key("ctrl-s")
+
+    # Wait until the file has been saved
+    machine.wait_for_file("/home/alice/foo.txt")
+
+    machine.screenshot("VSCodium")
+  '';
+})
diff --git a/nixos/tests/wasabibackend.nix b/nixos/tests/wasabibackend.nix
index d169ad152722a..1832698ab698c 100644
--- a/nixos/tests/wasabibackend.nix
+++ b/nixos/tests/wasabibackend.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "wasabibackend";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ mmahut ];
   };
 
diff --git a/nixos/tests/web-servers/unit-php.nix b/nixos/tests/web-servers/unit-php.nix
index 2a0a5bdaa5d51..24d6f5f16a72c 100644
--- a/nixos/tests/web-servers/unit-php.nix
+++ b/nixos/tests/web-servers/unit-php.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({pkgs, ...}:
 
 in {
   name = "unit-php-test";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ izorkin ];
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
 
   machine = { config, lib, pkgs, ... }: {
     services.unit = {
@@ -47,7 +47,7 @@ in {
     machine.wait_for_unit("unit.service")
 
     # Check so we get an evaluated PHP back
-    response = machine.succeed("curl -vvv -s http://127.0.0.1:9074/")
+    response = machine.succeed("curl -f -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
diff --git a/nixos/tests/wireguard/basic.nix b/nixos/tests/wireguard/basic.nix
index 25d706ae2e52c..a31e92e8649de 100644
--- a/nixos/tests/wireguard/basic.nix
+++ b/nixos/tests/wireguard/basic.nix
@@ -6,7 +6,7 @@ import ../make-test-python.nix ({ pkgs, lib, ...} :
   in
   {
     name = "wireguard";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ ma27 ];
     };
 
diff --git a/nixos/tests/wireguard/generated.nix b/nixos/tests/wireguard/generated.nix
index cdf15483265cd..84a35d29b4535 100644
--- a/nixos/tests/wireguard/generated.nix
+++ b/nixos/tests/wireguard/generated.nix
@@ -1,7 +1,7 @@
 { kernelPackages ? null }:
 import ../make-test-python.nix ({ pkgs, lib, ... } : {
   name = "wireguard-generated";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 grahamc ];
   };
 
diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix
index c47175ceafc83..93dc84a8768e5 100644
--- a/nixos/tests/wireguard/namespaces.nix
+++ b/nixos/tests/wireguard/namespaces.nix
@@ -17,7 +17,7 @@ in
 
 import ../make-test-python.nix ({ pkgs, lib, ... } : {
   name = "wireguard-with-namespaces";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ asymmetric ];
   };
 
diff --git a/nixos/tests/wireguard/wg-quick.nix b/nixos/tests/wireguard/wg-quick.nix
index 5472d21cd1ec2..8cf8c307de385 100644
--- a/nixos/tests/wireguard/wg-quick.nix
+++ b/nixos/tests/wireguard/wg-quick.nix
@@ -7,7 +7,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
   in
   {
     name = "wg-quick";
-    meta = with pkgs.stdenv.lib.maintainers; {
+    meta = with pkgs.lib.maintainers; {
       maintainers = [ xwvvvvwx ];
     };
 
diff --git a/nixos/tests/wordpress.nix b/nixos/tests/wordpress.nix
index b7449859f7e62..a5c10c2de741d 100644
--- a/nixos/tests/wordpress.nix
+++ b/nixos/tests/wordpress.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "wordpress";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [
       flokli
       grahamc # under duress!
@@ -40,7 +40,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
     with subtest("website returns welcome screen"):
         for site_name in site_names:
-            assert "Welcome to the famous" in machine.succeed(f"curl -L {site_name}")
+            assert "Welcome to the famous" in machine.succeed(f"curl -fL {site_name}")
 
     with subtest("wordpress-init went through"):
         for site_name in site_names:
diff --git a/nixos/tests/xautolock.nix b/nixos/tests/xautolock.nix
index 4a8d3f4cebf7c..2d29f80b3fee8 100644
--- a/nixos/tests/xautolock.nix
+++ b/nixos/tests/xautolock.nix
@@ -4,7 +4,7 @@ with lib;
 
 {
   name = "xautolock";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ ma27 ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
 
   nodes.machine = {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
index 56baae8b9d3cd..078cd2118107c 100644
--- a/nixos/tests/xmonad.nix
+++ b/nixos/tests/xmonad.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "xmonad";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ nequissimus ];
   };
 
@@ -14,9 +14,16 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       extraPackages = with pkgs.haskellPackages; haskellPackages: [ xmobar ];
       config = ''
         import XMonad
+        import XMonad.Operations (restart)
         import XMonad.Util.EZConfig
-        main = launch $ def `additionalKeysP` myKeys
-        myKeys = [ ("M-C-x", spawn "xterm") ]
+        import XMonad.Util.SessionStart
+
+        main = launch $ def { startupHook = startup } `additionalKeysP` myKeys
+
+        startup = isSessionStart >>= \sessInit ->
+          if sessInit then setSessionStarted else spawn "xterm"
+
+        myKeys = [ ("M-C-x", spawn "xterm"), ("M-q", restart "xmonad" True) ]
       '';
     };
   };
@@ -30,12 +37,11 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     machine.send_key("alt-ctrl-x")
     machine.wait_for_window("${user.name}.*machine")
     machine.sleep(1)
-    machine.screenshot("terminal")
-    machine.wait_until_succeeds("xmonad --restart")
+    machine.screenshot("terminal1")
+    machine.send_key("alt-q")
     machine.sleep(3)
-    machine.send_key("alt-shift-ret")
     machine.wait_for_window("${user.name}.*machine")
     machine.sleep(1)
-    machine.screenshot("terminal")
+    machine.screenshot("terminal2")
   '';
 })
diff --git a/nixos/tests/xmpp/ejabberd.nix b/nixos/tests/xmpp/ejabberd.nix
index 2b09f99f5fd99..7926fe80de2fd 100644
--- a/nixos/tests/xmpp/ejabberd.nix
+++ b/nixos/tests/xmpp/ejabberd.nix
@@ -1,6 +1,6 @@
 import ../make-test-python.nix ({ pkgs, ... }: {
   name = "ejabberd";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ajs124 ];
   };
   nodes = {
diff --git a/nixos/tests/xmpp/prosody.nix b/nixos/tests/xmpp/prosody.nix
index e7755e24bab41..2eb06d88287f0 100644
--- a/nixos/tests/xmpp/prosody.nix
+++ b/nixos/tests/xmpp/prosody.nix
@@ -85,7 +85,7 @@ in import ../make-test-python.nix {
     server.succeed('prosodyctl status | grep "Prosody is running"')
 
     server.succeed("create-prosody-users")
-    client.succeed('send-message 2>&1 | grep "XMPP SCRIPT TEST SUCCESS"')
+    client.succeed("send-message")
     server.succeed("delete-prosody-users")
   '';
 }
diff --git a/nixos/tests/xmpp/xmpp-sendmessage.nix b/nixos/tests/xmpp/xmpp-sendmessage.nix
index 30945e68300a8..47a77f524c6af 100644
--- a/nixos/tests/xmpp/xmpp-sendmessage.nix
+++ b/nixos/tests/xmpp/xmpp-sendmessage.nix
@@ -23,8 +23,26 @@ class CthonTest(ClientXMPP):
     def __init__(self, jid, password):
         ClientXMPP.__init__(self, jid, password)
         self.add_event_handler("session_start", self.session_start)
+        self.test_succeeded = False
 
     async def session_start(self, event):
+        try:
+            # Exceptions in event handlers are printed to stderr but not
+            # propagated, they do not make the script terminate with a non-zero
+            # exit code. We use the `test_succeeded` flag as a workaround and
+            # check it later at the end of the script to exit with a proper
+            # exit code.
+            # Additionally, this flag ensures that this event handler has been
+            # actually run by ClientXMPP, which may well not be the case.
+            await self.test_xmpp_server()
+            self.test_succeeded = True
+        finally:
+            # Even if an exception happens in `test_xmpp_server()`, we still
+            # need to disconnect explicitly, otherwise the process will hang
+            # forever.
+            self.disconnect(wait=True)
+
+    async def test_xmpp_server(self):
         log = logging.getLogger(__name__)
         self.send_presence()
         self.get_roster()
@@ -42,11 +60,12 @@ class CthonTest(ClientXMPP):
             log.error("ERROR: Cannot run upload command. XEP_0363 seems broken")
             sys.exit(1)
         log.info('Upload success!')
+
         # Test MUC
-        self.plugin['xep_0045'].join_muc('testMucRoom', 'cthon98', wait=True)
+        # TODO: use join_muc_wait() after slixmpp 1.8.0 is released.
+        self.plugin['xep_0045'].join_muc('testMucRoom', 'cthon98')
         log.info('MUC join success!')
         log.info('XMPP SCRIPT TEST SUCCESS')
-        self.disconnect(wait=True)
 
 
 if __name__ == '__main__':
@@ -62,4 +81,7 @@ if __name__ == '__main__':
     ct.register_plugin('xep_0045')
     ct.connect(("server", 5222))
     ct.process(forever=False)
+
+    if not ct.test_succeeded:
+        sys.exit(1)
 ''
diff --git a/nixos/tests/xrdp.nix b/nixos/tests/xrdp.nix
index 6d7f2b9249ffa..92eb7d4772ef2 100644
--- a/nixos/tests/xrdp.nix
+++ b/nixos/tests/xrdp.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "xrdp";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ volth ];
   };
 
diff --git a/nixos/tests/xss-lock.nix b/nixos/tests/xss-lock.nix
index b77bbbbb3c4e7..71f56e32c58a2 100644
--- a/nixos/tests/xss-lock.nix
+++ b/nixos/tests/xss-lock.nix
@@ -4,7 +4,7 @@ with lib;
 
 {
   name = "xss-lock";
-  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ ma27 ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
 
   nodes = {
     simple = {
diff --git a/nixos/tests/xterm.nix b/nixos/tests/xterm.nix
new file mode 100644
index 0000000000000..078d1dca96423
--- /dev/null
+++ b/nixos/tests/xterm.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "xterm";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      imports = [ ./common/x11.nix ];
+      services.xserver.desktopManager.xterm.enable = false;
+    };
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.succeed("DISPLAY=:0 xterm -title testterm -class testterm -fullscreen &")
+      machine.sleep(2)
+      machine.send_chars("echo $XTERM_VERSION >> /tmp/xterm_version\n")
+      machine.wait_for_file("/tmp/xterm_version")
+      assert "${pkgs.xterm.version}" in machine.succeed("cat /tmp/xterm_version")
+      machine.screenshot("window")
+    '';
+})
diff --git a/nixos/tests/yabar.nix b/nixos/tests/yabar.nix
index b374ef2968074..545fe544d534e 100644
--- a/nixos/tests/yabar.nix
+++ b/nixos/tests/yabar.nix
@@ -4,7 +4,7 @@ with lib;
 
 {
   name = "yabar";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 ];
   };
 
diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix
index 1d7541308b481..0b58ad29aa2b1 100644
--- a/nixos/tests/yggdrasil.nix
+++ b/nixos/tests/yggdrasil.nix
@@ -23,7 +23,7 @@ let
 
 in import ./make-test-python.nix ({ pkgs, ...} : {
   name = "yggdrasil";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with pkgs.lib.maintainers; {
     maintainers = [ gazally ];
   };
 
diff --git a/nixos/tests/yq.nix b/nixos/tests/yq.nix
new file mode 100644
index 0000000000000..cdcb3d6e2462d
--- /dev/null
+++ b/nixos/tests/yq.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "yq";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.yq = { pkgs, ... }: { environment.systemPackages = with pkgs; [ jq yq ]; };
+
+  testScript = ''
+    assert "hello:\n  foo: bar\n" in yq.succeed(
+        'echo \'{"hello":{"foo":"bar"}}\' | yq -y .'
+    )
+  '';
+})
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
index 87e6c900c98e0..ba5eb7cd528eb 100644
--- a/nixos/tests/zfs.nix
+++ b/nixos/tests/zfs.nix
@@ -8,17 +8,17 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 let
 
   makeZfsTest = name:
-    { kernelPackage ? pkgs.linuxPackages_latest
+    { kernelPackage ? if enableUnstable then pkgs.linuxPackages_latest else pkgs.linuxPackages
     , enableUnstable ? false
     , extraTest ? ""
     }:
     makeTest {
       name = "zfs-" + name;
-      meta = with pkgs.stdenv.lib.maintainers; {
+      meta = with pkgs.lib.maintainers; {
         maintainers = [ adisbladis ];
       };
 
-      machine = { pkgs, ... }: {
+      machine = { pkgs, lib, ... }: {
         virtualisation.emptyDiskImages = [ 4096 ];
         networking.hostId = "deadbeef";
         boot.kernelPackages = kernelPackage;
@@ -26,6 +26,24 @@ let
         boot.zfs.enableUnstable = enableUnstable;
 
         environment.systemPackages = [ pkgs.parted ];
+
+        # Setup regular fileSystems machinery to ensure forceImportAll can be
+        # tested via the regular service units.
+        virtualisation.fileSystems = {
+          "/forcepool" = {
+            device = "forcepool";
+            fsType = "zfs";
+            options = [ "noauto" ];
+          };
+        };
+
+        # forcepool doesn't exist at first boot, and we need to manually test
+        # the import after tweaking the hostId.
+        systemd.services.zfs-import-forcepool.wantedBy = lib.mkVMOverride [];
+        systemd.targets.zfs.wantedBy = lib.mkVMOverride [];
+        boot.zfs.forceImportAll = true;
+        # /dev/disk/by-id doesn't get populated in the NixOS test framework
+        boot.zfs.devNodes = "/dev/disk/by-uuid";
       };
 
       testScript = ''
@@ -57,6 +75,21 @@ let
             "zpool destroy rpool",
             "udevadm settle",
         )
+
+        with subtest("boot.zfs.forceImportAll works"):
+            machine.succeed(
+                "rm /etc/hostid",
+                "zgenhostid deadcafe",
+                "zpool create forcepool /dev/vdb1 -O mountpoint=legacy",
+            )
+            machine.shutdown()
+            machine.start()
+            machine.succeed("udevadm settle")
+            machine.fail("zpool import forcepool")
+            machine.succeed(
+                "systemctl start zfs-import-forcepool.service",
+                "mount -t zfs forcepool /tmp/mnt",
+            )
       '' + extraTest;
 
     };
diff --git a/nixos/tests/zookeeper.nix b/nixos/tests/zookeeper.nix
index 42cf20b39c522..0ee2673886a74 100644
--- a/nixos/tests/zookeeper.nix
+++ b/nixos/tests/zookeeper.nix
@@ -1,7 +1,12 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} :
+let
+
+  perlEnv = pkgs.perl.withPackages (p: [p.NetZooKeeper]);
+
+in {
   name = "zookeeper";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ nequissimus ];
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ztzg ];
   };
 
   nodes = {
@@ -30,5 +35,12 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     server.wait_until_succeeds(
         "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 get /foo | grep hello"
     )
+
+    server.wait_until_succeeds(
+        "${perlEnv}/bin/perl -E 'use Net::ZooKeeper qw(:acls); $z=Net::ZooKeeper->new(q(localhost:2181)); $z->create(qw(/perl foo acl), ZOO_OPEN_ACL_UNSAFE) || die $z->get_error()'"
+    )
+    server.wait_until_succeeds(
+        "${perlEnv}/bin/perl -E 'use Net::ZooKeeper qw(:acls); $z=Net::ZooKeeper->new(q(localhost:2181)); $z->get(qw(/perl)) eq qw(foo) || die $z->get_error()'"
+    )
   '';
 })
diff --git a/nixos/tests/zsh-history.nix b/nixos/tests/zsh-history.nix
index 4380ec9adfd25..3109c3f650818 100644
--- a/nixos/tests/zsh-history.nix
+++ b/nixos/tests/zsh-history.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "zsh-history";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ kampka ];
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
   };
 
   nodes.default = { ... }: {