about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/declarative-containers.section.md2
-rw-r--r--nixos/doc/manual/administration/imperative-containers.section.md4
-rw-r--r--nixos/doc/manual/configuration/adding-custom-packages.section.md35
-rw-r--r--nixos/doc/manual/configuration/xfce.chapter.md11
-rw-r--r--nixos/doc/manual/default.nix10
-rw-r--r--nixos/doc/manual/development/activation-script.section.md72
-rw-r--r--nixos/doc/manual/development/development.xml2
-rw-r--r--nixos/doc/manual/development/option-declarations.section.md46
-rw-r--r--nixos/doc/manual/development/option-types.section.md50
-rw-r--r--nixos/doc/manual/development/running-nixos-tests.section.md4
-rw-r--r--nixos/doc/manual/development/settings-options.section.md59
-rw-r--r--nixos/doc/manual/development/unit-handling.section.md62
-rw-r--r--nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md53
-rw-r--r--nixos/doc/manual/development/writing-modules.chapter.md42
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.section.md127
-rw-r--r--nixos/doc/manual/from_md/administration/declarative-containers.section.xml4
-rw-r--r--nixos/doc/manual/from_md/administration/imperative-containers.section.xml5
-rw-r--r--nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml116
-rw-r--r--nixos/doc/manual/from_md/configuration/xfce.chapter.xml14
-rw-r--r--nixos/doc/manual/from_md/development/activation-script.section.xml150
-rw-r--r--nixos/doc/manual/from_md/development/option-declarations.section.xml53
-rw-r--r--nixos/doc/manual/from_md/development/option-types.section.xml88
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests.section.xml4
-rw-r--r--nixos/doc/manual/from_md/development/settings-options.section.xml136
-rw-r--r--nixos/doc/manual/from_md/development/unit-handling.section.xml131
-rw-r--r--nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml122
-rw-r--r--nixos/doc/manual/from_md/development/writing-modules.chapter.xml49
-rw-r--r--nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml193
-rw-r--r--nixos/doc/manual/from_md/installation/building-nixos.chapter.xml (renamed from nixos/doc/manual/from_md/development/building-nixos.chapter.xml)39
-rw-r--r--nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml2
-rw-r--r--nixos/doc/manual/from_md/installation/installing-kexec.section.xml94
-rw-r--r--nixos/doc/manual/from_md/installation/installing-pxe.section.xml4
-rw-r--r--nixos/doc/manual/from_md/installation/installing-usb.section.xml2
-rw-r--r--nixos/doc/manual/from_md/installation/installing.chapter.xml15
-rw-r--r--nixos/doc/manual/from_md/installation/upgrading.chapter.xml16
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1803.section.xml8
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml48
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml2080
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2211.section.xml381
-rw-r--r--nixos/doc/manual/installation/building-nixos.chapter.md (renamed from nixos/doc/manual/development/building-nixos.chapter.md)34
-rw-r--r--nixos/doc/manual/installation/installation.xml1
-rw-r--r--nixos/doc/manual/installation/installing-from-other-distro.section.md2
-rw-r--r--nixos/doc/manual/installation/installing-kexec.section.md64
-rw-r--r--nixos/doc/manual/installation/installing-pxe.section.md4
-rw-r--r--nixos/doc/manual/installation/installing-usb.section.md2
-rw-r--r--nixos/doc/manual/installation/installing.chapter.md11
-rw-r--r--nixos/doc/manual/installation/upgrading.chapter.md16
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml26
-rw-r--r--nixos/doc/manual/man-pages.xml9
-rwxr-xr-xnixos/doc/manual/md-to-db.sh2
-rw-r--r--nixos/doc/manual/release-notes/release-notes.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-1803.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md22
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md756
-rw-r--r--nixos/doc/manual/release-notes/rl-2211.section.md139
-rw-r--r--nixos/lib/build-vms.nix2
-rw-r--r--nixos/lib/eval-config.nix5
-rw-r--r--nixos/lib/make-disk-image.nix1
-rw-r--r--nixos/lib/make-ext4-fs.nix9
-rw-r--r--nixos/lib/make-multi-disk-zfs-image.nix (renamed from nixos/lib/make-zfs-image.nix)2
-rw-r--r--nixos/lib/make-options-doc/default.nix36
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py163
-rw-r--r--nixos/lib/make-options-doc/options-to-docbook.xsl47
-rw-r--r--nixos/lib/make-single-disk-zfs-image.nix322
-rw-r--r--nixos/lib/qemu-common.nix4
-rw-r--r--nixos/lib/systemd-lib.nix212
-rw-r--r--nixos/lib/systemd-types.nix69
-rw-r--r--nixos/lib/systemd-unit-options.nix956
-rw-r--r--nixos/lib/test-driver/default.nix16
-rw-r--r--nixos/lib/test-driver/setup.py1
-rw-r--r--nixos/lib/test-driver/test_driver/driver.py3
-rw-r--r--nixos/lib/test-driver/test_driver/logger.py8
-rw-r--r--nixos/lib/test-driver/test_driver/machine.py52
-rw-r--r--nixos/lib/test-driver/test_driver/py.typed0
-rw-r--r--nixos/lib/test-driver/test_driver/vlan.py8
-rw-r--r--nixos/lib/test-script-prepend.py42
-rw-r--r--nixos/lib/testing-python.nix67
-rw-r--r--nixos/lib/utils.nix42
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix2
-rwxr-xr-xnixos/maintainers/scripts/ec2/create-amis.sh30
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-image.nix2
-rw-r--r--nixos/maintainers/scripts/openstack/openstack-image-zfs.nix101
-rw-r--r--nixos/maintainers/scripts/openstack/openstack-image.nix17
-rw-r--r--nixos/modules/config/console.nix36
-rw-r--r--nixos/modules/config/debug-info.nix19
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix17
-rw-r--r--nixos/modules/config/fonts/fonts.nix5
-rw-r--r--nixos/modules/config/i18n.nix18
-rw-r--r--nixos/modules/config/nsswitch.nix11
-rw-r--r--nixos/modules/config/qt5.nix72
-rw-r--r--nixos/modules/config/resolvconf.nix27
-rw-r--r--nixos/modules/config/terminfo.nix26
-rw-r--r--nixos/modules/config/update-users-groups.pl6
-rw-r--r--nixos/modules/config/users-groups.nix27
-rw-r--r--nixos/modules/config/xdg/icons.nix8
-rw-r--r--nixos/modules/config/xdg/portal.nix27
-rw-r--r--nixos/modules/config/xdg/portals/lxqt.nix49
-rw-r--r--nixos/modules/hardware/all-firmware.nix12
-rw-r--r--nixos/modules/hardware/ckb-next.nix2
-rw-r--r--nixos/modules/hardware/device-tree.nix18
-rw-r--r--nixos/modules/hardware/gpgsmartcards.nix2
-rw-r--r--nixos/modules/hardware/keyboard/uhk.nix21
-rw-r--r--nixos/modules/hardware/network/b43.nix4
-rw-r--r--nixos/modules/hardware/new-lg4ff.nix29
-rw-r--r--nixos/modules/hardware/raid/hpsa.nix7
-rw-r--r--nixos/modules/hardware/saleae-logic.nix25
-rw-r--r--nixos/modules/hardware/video/amdgpu-pro.nix3
-rw-r--r--nixos/modules/hardware/video/capture/mwprocapture.nix5
-rw-r--r--nixos/modules/hardware/video/nvidia.nix85
-rw-r--r--nixos/modules/hardware/video/webcam/facetimehd.nix20
-rw-r--r--nixos/modules/hardware/xone.nix23
-rw-r--r--nixos/modules/i18n/input-method/fcitx5.nix32
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix3
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-base.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix29
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix60
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma5.nix49
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix20
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix8
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix73
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix160
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt89
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc.nix163
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix172
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball.nix93
-rw-r--r--nixos/modules/installer/netboot/netboot.nix33
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64.nix15
-rw-r--r--nixos/modules/installer/sd-card/sd-image.nix15
-rw-r--r--nixos/modules/installer/tools/get-version-suffix7
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix10
-rw-r--r--nixos/modules/installer/tools/nixos-build-vms/build-vms.nix3
-rw-r--r--nixos/modules/installer/tools/nixos-enter.sh22
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl36
-rw-r--r--nixos/modules/installer/tools/tools.nix15
-rw-r--r--nixos/modules/misc/documentation.nix36
-rw-r--r--nixos/modules/misc/ids.nix33
-rw-r--r--nixos/modules/misc/label.nix5
-rw-r--r--nixos/modules/misc/locate.nix10
-rw-r--r--nixos/modules/misc/man-db.nix8
-rw-r--r--nixos/modules/misc/mandoc.nix4
-rw-r--r--nixos/modules/misc/nixpkgs.nix117
-rw-r--r--nixos/modules/misc/nixpkgs/test.nix61
-rw-r--r--nixos/modules/misc/version.nix74
-rw-r--r--nixos/modules/misc/wordlist.nix59
-rw-r--r--nixos/modules/module-list.nix108
-rw-r--r--nixos/modules/profiles/all-hardware.nix3
-rw-r--r--nixos/modules/profiles/installation-device.nix4
-rw-r--r--nixos/modules/profiles/minimal.nix3
-rw-r--r--nixos/modules/profiles/qemu-guest.nix4
-rw-r--r--nixos/modules/programs/_1password-gui.nix65
-rw-r--r--nixos/modules/programs/_1password.nix41
-rw-r--r--nixos/modules/programs/adb.nix3
-rw-r--r--nixos/modules/programs/atop.nix18
-rw-r--r--nixos/modules/programs/captive-browser.nix20
-rw-r--r--nixos/modules/programs/cfs-zen-tweaks.nix28
-rw-r--r--nixos/modules/programs/chromium.nix3
-rw-r--r--nixos/modules/programs/clickshare.nix21
-rw-r--r--nixos/modules/programs/dmrconfig.nix2
-rw-r--r--nixos/modules/programs/environment.nix4
-rw-r--r--nixos/modules/programs/firejail.nix4
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/programs/haguichi.nix15
-rw-r--r--nixos/modules/programs/k3b.nix52
-rw-r--r--nixos/modules/programs/k40-whisperer.nix40
-rw-r--r--nixos/modules/programs/kclock.nix13
-rw-r--r--nixos/modules/programs/kdeconnect.nix4
-rw-r--r--nixos/modules/programs/mininet.nix4
-rw-r--r--nixos/modules/programs/nbd.nix19
-rw-r--r--nixos/modules/programs/nethoscope.nix30
-rw-r--r--nixos/modules/programs/nix-ld.nix10
-rw-r--r--nixos/modules/programs/nncp.nix101
-rw-r--r--nixos/modules/programs/openvpn3.nix33
-rw-r--r--nixos/modules/programs/qt5ct.nix30
-rw-r--r--nixos/modules/programs/ssh.nix25
-rw-r--r--nixos/modules/programs/ssmtp.nix190
-rw-r--r--nixos/modules/programs/streamdeck-ui.nix28
-rw-r--r--nixos/modules/programs/sway.nix1
-rw-r--r--nixos/modules/programs/thefuck.nix13
-rw-r--r--nixos/modules/programs/thunar.nix45
-rw-r--r--nixos/modules/programs/xfconf.nix27
-rw-r--r--nixos/modules/programs/zsh/zsh-autosuggestions.nix21
-rw-r--r--nixos/modules/rename.nix13
-rw-r--r--nixos/modules/security/pam.nix146
-rw-r--r--nixos/modules/security/pam_mount.nix84
-rw-r--r--nixos/modules/security/polkit.nix6
-rw-r--r--nixos/modules/security/sudo.nix2
-rw-r--r--nixos/modules/security/systemd-confinement.nix15
-rw-r--r--nixos/modules/security/wrappers/default.nix5
-rw-r--r--nixos/modules/security/wrappers/wrapper.c38
-rw-r--r--nixos/modules/services/admin/pgadmin.nix127
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix9
-rw-r--r--nixos/modules/services/audio/mpd.nix1
-rw-r--r--nixos/modules/services/audio/navidrome.nix3
-rw-r--r--nixos/modules/services/audio/snapserver.nix48
-rw-r--r--nixos/modules/services/audio/squeezelite.nix38
-rw-r--r--nixos/modules/services/backup/automysqlbackup.nix2
-rw-r--r--nixos/modules/services/backup/borgmatic.nix5
-rw-r--r--nixos/modules/services/backup/btrbk.nix90
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix3
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix29
-rw-r--r--nixos/modules/services/backup/restic.nix174
-rw-r--r--nixos/modules/services/backup/zrepl.nix14
-rw-r--r--nixos/modules/services/cluster/corosync/default.nix112
-rw-r--r--nixos/modules/services/cluster/hadoop/conf.nix22
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix97
-rw-r--r--nixos/modules/services/cluster/hadoop/hdfs.nix295
-rw-r--r--nixos/modules/services/cluster/hadoop/yarn.nix98
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix24
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix6
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix4
-rw-r--r--nixos/modules/services/cluster/pacemaker/default.nix52
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix10
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix4
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix34
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix11
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix23
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix23
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/slave.nix16
-rw-r--r--nixos/modules/services/databases/cassandra.nix127
-rw-r--r--nixos/modules/services/databases/cockroachdb.nix66
-rw-r--r--nixos/modules/services/databases/couchdb.nix6
-rw-r--r--nixos/modules/services/databases/dgraph.nix148
-rw-r--r--nixos/modules/services/databases/dragonflydb.nix152
-rw-r--r--nixos/modules/services/databases/openldap.nix14
-rw-r--r--nixos/modules/services/databases/postgresql.nix3
-rw-r--r--nixos/modules/services/databases/redis.nix25
-rw-r--r--nixos/modules/services/databases/riak.nix162
-rw-r--r--nixos/modules/services/databases/virtuoso.nix99
-rw-r--r--nixos/modules/services/desktops/flatpak.nix2
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json2
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json120
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json81
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json7
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix18
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix33
-rw-r--r--nixos/modules/services/desktops/pipewire/wireplumber.nix42
-rw-r--r--nixos/modules/services/development/jupyter/default.nix4
-rw-r--r--nixos/modules/services/development/jupyter/kernel-options.nix9
-rw-r--r--nixos/modules/services/development/zammad.nix323
-rw-r--r--nixos/modules/services/display-managers/greetd.nix2
-rw-r--r--nixos/modules/services/editors/haste.nix86
-rw-r--r--nixos/modules/services/games/asf.nix89
-rw-r--r--nixos/modules/services/games/factorio.nix24
-rw-r--r--nixos/modules/services/games/minecraft-server.nix32
-rw-r--r--nixos/modules/services/hardware/argonone.nix58
-rw-r--r--nixos/modules/services/hardware/illum.nix1
-rw-r--r--nixos/modules/services/hardware/joycond.nix8
-rw-r--r--nixos/modules/services/hardware/udev.nix155
-rw-r--r--nixos/modules/services/hardware/udisks2.nix41
-rw-r--r--nixos/modules/services/hardware/undervolt.nix4
-rw-r--r--nixos/modules/services/hardware/usbrelayd.nix47
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix573
-rw-r--r--nixos/modules/services/home-automation/zigbee2mqtt.nix (renamed from nixos/modules/services/misc/zigbee2mqtt.nix)0
-rw-r--r--nixos/modules/services/logging/graylog.nix2
-rw-r--r--nixos/modules/services/logging/heartbeat.nix12
-rw-r--r--nixos/modules/services/logging/klogd.nix41
-rw-r--r--nixos/modules/services/logging/logrotate.nix299
-rw-r--r--nixos/modules/services/logging/logstash.nix2
-rw-r--r--nixos/modules/services/logging/syslog-ng.nix3
-rw-r--r--nixos/modules/services/mail/mailman.nix244
-rw-r--r--nixos/modules/services/mail/postfix.nix40
-rw-r--r--nixos/modules/services/mail/postfixadmin.nix4
-rw-r--r--nixos/modules/services/mail/public-inbox.nix579
-rw-r--r--nixos/modules/services/mail/roundcube.nix2
-rw-r--r--nixos/modules/services/mail/schleuder.nix162
-rw-r--r--nixos/modules/services/mail/spamassassin.nix2
-rw-r--r--nixos/modules/services/matrix/appservice-discord.nix (renamed from nixos/modules/services/misc/matrix-appservice-discord.nix)0
-rw-r--r--nixos/modules/services/matrix/appservice-irc.nix (renamed from nixos/modules/services/misc/matrix-appservice-irc.nix)3
-rw-r--r--nixos/modules/services/matrix/conduit.nix (renamed from nixos/modules/services/misc/matrix-conduit.nix)0
-rw-r--r--nixos/modules/services/matrix/dendrite.nix (renamed from nixos/modules/services/misc/dendrite.nix)136
-rw-r--r--nixos/modules/services/matrix/mautrix-facebook.nix (renamed from nixos/modules/services/misc/mautrix-facebook.nix)0
-rw-r--r--nixos/modules/services/matrix/mautrix-telegram.nix (renamed from nixos/modules/services/misc/mautrix-telegram.nix)0
-rw-r--r--nixos/modules/services/matrix/mjolnir.xml4
-rw-r--r--nixos/modules/services/matrix/synapse-log_config.yaml (renamed from nixos/modules/services/misc/matrix-synapse-log_config.yaml)0
-rw-r--r--nixos/modules/services/matrix/synapse.nix774
-rw-r--r--nixos/modules/services/matrix/synapse.xml276
-rw-r--r--nixos/modules/services/misc/airsonic.nix8
-rw-r--r--nixos/modules/services/misc/autorandr.nix310
-rw-r--r--nixos/modules/services/misc/dictd.nix4
-rw-r--r--nixos/modules/services/misc/etebase-server.nix2
-rw-r--r--nixos/modules/services/misc/ethminer.nix8
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix53
-rw-r--r--nixos/modules/services/misc/gitea.nix27
-rw-r--r--nixos/modules/services/misc/gitit.nix2
-rw-r--r--nixos/modules/services/misc/gitlab.nix81
-rw-r--r--nixos/modules/services/misc/gollum.nix23
-rw-r--r--nixos/modules/services/misc/heisenbridge.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix416
-rw-r--r--nixos/modules/services/misc/input-remapper.nix30
-rw-r--r--nixos/modules/services/misc/jellyfin.nix68
-rw-r--r--nixos/modules/services/misc/libreddit.nix44
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix844
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml230
-rw-r--r--nixos/modules/services/misc/mbpfan.nix24
-rw-r--r--nixos/modules/services/misc/mediatomb.nix1
-rw-r--r--nixos/modules/services/misc/moonraker.nix40
-rw-r--r--nixos/modules/services/misc/nitter.nix17
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix60
-rw-r--r--nixos/modules/services/misc/nix-gc.nix12
-rw-r--r--nixos/modules/services/misc/nix-optimise.nix10
-rw-r--r--nixos/modules/services/misc/paperless.nix (renamed from nixos/modules/services/misc/paperless-ng.nix)110
-rw-r--r--nixos/modules/services/misc/persistent-evdev.nix60
-rw-r--r--nixos/modules/services/misc/plex.nix13
-rw-r--r--nixos/modules/services/misc/polaris.nix151
-rw-r--r--nixos/modules/services/misc/radarr.nix10
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix236
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix81
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix127
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix217
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix175
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix120
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix187
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix124
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix213
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix135
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix2
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix163
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix25
-rw-r--r--nixos/modules/services/misc/taskserver/doc.xml2
-rw-r--r--nixos/modules/services/misc/taskserver/helper-tool.py10
-rw-r--r--nixos/modules/services/misc/uhub.nix14
-rw-r--r--nixos/modules/services/misc/zoneminder.nix4
-rw-r--r--nixos/modules/services/misc/zookeeper.nix9
-rw-r--r--nixos/modules/services/monitoring/collectd.nix56
-rw-r--r--nixos/modules/services/monitoring/grafana-agent.nix143
-rw-r--r--nixos/modules/services/monitoring/grafana.nix90
-rw-r--r--nixos/modules/services/monitoring/graphite.nix160
-rw-r--r--nixos/modules/services/monitoring/mimir.nix67
-rw-r--r--nixos/modules/services/monitoring/nagios.nix4
-rw-r--r--nixos/modules/services/monitoring/netdata.nix6
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix163
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix43
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.xml21
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bird.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix117
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/kea.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mail.nix21
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/node.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postfix.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pve.nix118
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/systemd.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/varnish.nix5
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix6
-rw-r--r--nixos/modules/services/network-filesystems/glusterfs.nix3
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix50
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix1
-rw-r--r--nixos/modules/services/networking/amuled.nix2
-rw-r--r--nixos/modules/services/networking/asterisk.nix54
-rw-r--r--nixos/modules/services/networking/bird-lg.nix269
-rw-r--r--nixos/modules/services/networking/bird.nix181
-rw-r--r--nixos/modules/services/networking/bitlbee.nix1
-rw-r--r--nixos/modules/services/networking/blocky.nix40
-rw-r--r--nixos/modules/services/networking/cloudflare-dyndns.nix93
-rw-r--r--nixos/modules/services/networking/consul.nix38
-rw-r--r--nixos/modules/services/networking/create_ap.nix50
-rw-r--r--nixos/modules/services/networking/ddclient.nix9
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix2
-rw-r--r--nixos/modules/services/networking/dhcpd.nix8
-rw-r--r--nixos/modules/services/networking/envoy.nix84
-rw-r--r--nixos/modules/services/networking/eternal-terminal.nix2
-rw-r--r--nixos/modules/services/networking/expressvpn.nix29
-rw-r--r--nixos/modules/services/networking/firewall.nix6
-rw-r--r--nixos/modules/services/networking/flannel.nix11
-rw-r--r--nixos/modules/services/networking/frr.nix11
-rw-r--r--nixos/modules/services/networking/gateone.nix4
-rw-r--r--nixos/modules/services/networking/headscale.nix2
-rw-r--r--nixos/modules/services/networking/https-dns-proxy.nix128
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix16
-rw-r--r--nixos/modules/services/networking/i2pd.nix15
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/ircd.conf2
-rw-r--r--nixos/modules/services/networking/iwd.nix21
-rw-r--r--nixos/modules/services/networking/kea.nix82
-rw-r--r--nixos/modules/services/networking/lokinet.nix157
-rw-r--r--nixos/modules/services/networking/lxd-image-server.nix18
-rw-r--r--nixos/modules/services/networking/minidlna.nix260
-rw-r--r--nixos/modules/services/networking/mosquitto.nix50
-rw-r--r--nixos/modules/services/networking/mozillavpn.nix19
-rw-r--r--nixos/modules/services/networking/multipath.nix15
-rw-r--r--nixos/modules/services/networking/murmur.nix15
-rw-r--r--nixos/modules/services/networking/mxisd.nix17
-rw-r--r--nixos/modules/services/networking/nbd.nix159
-rw-r--r--nixos/modules/services/networking/ncdns.nix4
-rw-r--r--nixos/modules/services/networking/nebula.nix1
-rw-r--r--nixos/modules/services/networking/networkmanager.nix110
-rw-r--r--nixos/modules/services/networking/nsd.nix20
-rw-r--r--nixos/modules/services/networking/ntopng.nix63
-rw-r--r--nixos/modules/services/networking/openconnect.nix143
-rw-r--r--nixos/modules/services/networking/openfire.nix56
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix19
-rw-r--r--nixos/modules/services/networking/powerdns.nix4
-rw-r--r--nixos/modules/services/networking/pptpd.nix2
-rw-r--r--nixos/modules/services/networking/prayer.nix2
-rw-r--r--nixos/modules/services/networking/prosody.nix80
-rw-r--r--nixos/modules/services/networking/r53-ddns.nix72
-rw-r--r--nixos/modules/services/networking/radicale.nix2
-rw-r--r--nixos/modules/services/networking/radvd.nix17
-rw-r--r--nixos/modules/services/networking/routedns.nix84
-rw-r--r--nixos/modules/services/networking/searx.nix8
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix43
-rw-r--r--nixos/modules/services/networking/snowflake-proxy.nix81
-rw-r--r--nixos/modules/services/networking/squid.nix27
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix6
-rw-r--r--nixos/modules/services/networking/supplicant.nix6
-rw-r--r--nixos/modules/services/networking/syncplay.nix18
-rw-r--r--nixos/modules/services/networking/syncthing.nix124
-rw-r--r--nixos/modules/services/networking/tailscale.nix50
-rw-r--r--nixos/modules/services/networking/tetrd.nix2
-rw-r--r--nixos/modules/services/networking/tox-node.nix7
-rw-r--r--nixos/modules/services/networking/trickster.nix20
-rw-r--r--nixos/modules/services/networking/unbound.nix1
-rw-r--r--nixos/modules/services/networking/unifi.nix6
-rw-r--r--nixos/modules/services/networking/uptermd.nix109
-rw-r--r--nixos/modules/services/networking/vsftpd.nix1
-rw-r--r--nixos/modules/services/networking/wg-quick.nix48
-rw-r--r--nixos/modules/services/networking/wireguard.nix10
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix2
-rw-r--r--nixos/modules/services/networking/xl2tpd.nix6
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix31
-rw-r--r--nixos/modules/services/networking/zeronet.nix13
-rw-r--r--nixos/modules/services/security/clamav.nix9
-rw-r--r--nixos/modules/services/security/fprot.nix82
-rw-r--r--nixos/modules/services/security/infnoise.nix60
-rw-r--r--nixos/modules/services/security/kanidm.nix345
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix13
-rw-r--r--nixos/modules/services/security/opensnitch.nix103
-rw-r--r--nixos/modules/services/security/pass-secret-service.nix27
-rw-r--r--nixos/modules/services/security/privacyidea.nix65
-rw-r--r--nixos/modules/services/security/sshguard.nix2
-rw-r--r--nixos/modules/services/security/sslmate-agent.nix32
-rw-r--r--nixos/modules/services/security/step-ca.nix15
-rw-r--r--nixos/modules/services/security/tor.nix13
-rw-r--r--nixos/modules/services/security/vault.nix28
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix76
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix19
-rw-r--r--nixos/modules/services/system/earlyoom.nix230
-rw-r--r--nixos/modules/services/system/localtime.nix34
-rw-r--r--nixos/modules/services/system/nscd.nix25
-rw-r--r--nixos/modules/services/system/systembus-notify.nix27
-rw-r--r--nixos/modules/services/tracing/tempo.nix68
-rw-r--r--nixos/modules/services/ttys/kmscon.nix29
-rw-r--r--nixos/modules/services/video/epgstation/default.nix348
-rw-r--r--nixos/modules/services/video/epgstation/streaming.json237
-rw-r--r--nixos/modules/services/video/unifi-video.nix168
-rw-r--r--nixos/modules/services/wayland/cage.nix4
-rw-r--r--nixos/modules/services/web-apps/atlassian/confluence.nix59
-rw-r--r--nixos/modules/services/web-apps/atlassian/crowd.nix33
-rw-r--r--nixos/modules/services/web-apps/atlassian/jira.nix49
-rw-r--r--nixos/modules/services/web-apps/calibre-web.nix2
-rw-r--r--nixos/modules/services/web-apps/cryptpad.nix54
-rw-r--r--nixos/modules/services/web-apps/discourse.nix12
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix6
-rw-r--r--nixos/modules/services/web-apps/galene.nix29
-rw-r--r--nixos/modules/services/web-apps/grocy.nix6
-rw-r--r--nixos/modules/services/web-apps/healthchecks.nix249
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix43
-rw-r--r--nixos/modules/services/web-apps/invoiceplane.nix3
-rw-r--r--nixos/modules/services/web-apps/jirafeau.nix2
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix27
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix870
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml142
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix2
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix11
-rw-r--r--nixos/modules/services/web-apps/matomo.nix1
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix46
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/netbox.nix265
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix202
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml2
-rw-r--r--nixos/modules/services/web-apps/nifi.nix318
-rw-r--r--nixos/modules/services/web-apps/onlyoffice.nix288
-rw-r--r--nixos/modules/services/web-apps/openwebrx.nix4
-rw-r--r--nixos/modules/services/web-apps/peertube.nix28
-rw-r--r--nixos/modules/services/web-apps/phylactery.nix51
-rw-r--r--nixos/modules/services/web-apps/plantuml-server.nix21
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix16
-rw-r--r--nixos/modules/services/web-apps/rss-bridge.nix2
-rw-r--r--nixos/modules/services/web-apps/snipe-it.nix493
-rw-r--r--nixos/modules/services/web-apps/timetagger.nix80
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix1
-rw-r--r--nixos/modules/services/web-servers/agate.nix148
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix21
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix2
-rw-r--r--nixos/modules/services/web-servers/hydron.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix45
-rw-r--r--nixos/modules/services/web-servers/nginx/gitweb.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix11
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix19
-rw-r--r--nixos/modules/services/web-servers/pomerium.nix10
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix7
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/enlightenment.nix5
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.nix172
-rw-r--r--nixos/modules/services/x11/desktop-managers/lumina.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/lxqt.nix12
-rw-r--r--nixos/modules/services/x11/desktop-managers/mate.nix46
-rw-r--r--nixos/modules/services/x11/desktop-managers/none.nix49
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix70
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml6
-rw-r--r--nixos/modules/services/x11/desktop-managers/phosh.nix (renamed from nixos/modules/programs/phosh.nix)100
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix238
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix34
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix1
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix11
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/xpra.nix9
-rw-r--r--nixos/modules/services/x11/picom.nix15
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix15
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix33
-rw-r--r--nixos/modules/services/x11/xserver.nix16
-rwxr-xr-x[-rw-r--r--]nixos/modules/system/activation/switch-to-configuration.pl830
-rw-r--r--nixos/modules/system/activation/top-level.nix19
-rw-r--r--nixos/modules/system/boot/kernel.nix7
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix26
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl8
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix2
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py58
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix2
-rw-r--r--nixos/modules/system/boot/luksroot.nix81
-rw-r--r--nixos/modules/system/boot/modprobe.nix22
-rw-r--r--nixos/modules/system/boot/networkd.nix201
-rw-r--r--nixos/modules/system/boot/plymouth.nix102
-rw-r--r--nixos/modules/system/boot/resolved.nix63
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh11
-rw-r--r--nixos/modules/system/boot/stage-1.nix96
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh164
-rw-r--r--nixos/modules/system/boot/stage-2.nix41
-rw-r--r--nixos/modules/system/boot/systemd.nix760
-rw-r--r--nixos/modules/system/boot/systemd/coredump.nix57
-rw-r--r--nixos/modules/system/boot/systemd/initrd-secrets.nix36
-rw-r--r--nixos/modules/system/boot/systemd/initrd.nix502
-rw-r--r--nixos/modules/system/boot/systemd/journald.nix131
-rw-r--r--nixos/modules/system/boot/systemd/logind.nix117
-rw-r--r--nixos/modules/system/boot/systemd/nspawn.nix (renamed from nixos/modules/system/boot/systemd-nspawn.nix)22
-rw-r--r--nixos/modules/system/boot/systemd/shutdown.nix58
-rw-r--r--nixos/modules/system/boot/systemd/tmpfiles.nix120
-rw-r--r--nixos/modules/system/boot/systemd/user.nix158
-rw-r--r--nixos/modules/system/boot/timesyncd.nix22
-rw-r--r--nixos/modules/system/boot/tmp.nix7
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix124
-rw-r--r--nixos/modules/tasks/bcache.nix22
-rw-r--r--nixos/modules/tasks/filesystems.nix34
-rw-r--r--nixos/modules/tasks/filesystems/btrfs.nix6
-rw-r--r--nixos/modules/tasks/filesystems/cifs.nix2
-rw-r--r--nixos/modules/tasks/filesystems/ext.nix14
-rw-r--r--nixos/modules/tasks/filesystems/f2fs.nix2
-rw-r--r--nixos/modules/tasks/filesystems/jfs.nix2
-rw-r--r--nixos/modules/tasks/filesystems/reiserfs.nix2
-rw-r--r--nixos/modules/tasks/filesystems/unionfs-fuse.nix15
-rw-r--r--nixos/modules/tasks/filesystems/vfat.nix2
-rw-r--r--nixos/modules/tasks/filesystems/xfs.nix4
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix245
-rw-r--r--nixos/modules/tasks/lvm.nix63
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix13
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix58
-rw-r--r--nixos/modules/tasks/network-interfaces.nix81
-rw-r--r--nixos/modules/tasks/swraid.nix44
-rw-r--r--nixos/modules/testing/test-instrumentation.nix48
-rw-r--r--nixos/modules/virtualisation/amazon-ec2-amis.nix50
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix3
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix2
-rw-r--r--nixos/modules/virtualisation/anbox.nix3
-rw-r--r--nixos/modules/virtualisation/appvm.nix49
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix14
-rw-r--r--nixos/modules/virtualisation/azure-common.nix6
-rw-r--r--nixos/modules/virtualisation/containerd.nix1
-rw-r--r--nixos/modules/virtualisation/cri-o.nix4
-rw-r--r--nixos/modules/virtualisation/digital-ocean-init.nix2
-rw-r--r--nixos/modules/virtualisation/docker-rootless.nix1
-rw-r--r--nixos/modules/virtualisation/docker.nix1
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix55
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix8
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix41
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix119
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix46
-rw-r--r--nixos/modules/virtualisation/openstack-metadata-fetcher.nix9
-rw-r--r--nixos/modules/virtualisation/openstack-options.nix71
-rw-r--r--nixos/modules/virtualisation/parallels-guest.nix61
-rw-r--r--nixos/modules/virtualisation/podman/default.nix11
-rw-r--r--nixos/modules/virtualisation/proxmox-image.nix22
-rw-r--r--nixos/modules/virtualisation/proxmox-lxc.nix75
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix130
-rw-r--r--nixos/modules/virtualisation/railcar.nix124
-rw-r--r--nixos/modules/virtualisation/vmware-guest.nix1
-rw-r--r--nixos/modules/virtualisation/vmware-host.nix166
-rw-r--r--nixos/modules/virtualisation/waydroid.nix6
-rw-r--r--nixos/modules/virtualisation/xen-dom0.nix6
-rw-r--r--nixos/release-combined.nix6
-rw-r--r--nixos/release-small.nix6
-rw-r--r--nixos/release.nix78
-rw-r--r--nixos/tests/3proxy.nix6
-rw-r--r--nixos/tests/acme.nix2
-rw-r--r--nixos/tests/aesmd.nix2
-rw-r--r--nixos/tests/agda.nix2
-rw-r--r--nixos/tests/airsonic.nix5
-rw-r--r--nixos/tests/all-terminfo.nix31
-rw-r--r--nixos/tests/all-tests.nix98
-rw-r--r--nixos/tests/amazon-init-shell.nix2
-rw-r--r--nixos/tests/apfs.nix2
-rw-r--r--nixos/tests/apparmor.nix2
-rw-r--r--nixos/tests/atd.nix2
-rw-r--r--nixos/tests/atop.nix20
-rw-r--r--nixos/tests/avahi.nix2
-rw-r--r--nixos/tests/bazarr.nix2
-rw-r--r--nixos/tests/bcachefs.nix13
-rw-r--r--nixos/tests/beanstalkd.nix2
-rw-r--r--nixos/tests/bees.nix2
-rw-r--r--nixos/tests/bind.nix2
-rw-r--r--nixos/tests/bird.nix132
-rw-r--r--nixos/tests/bitcoind.nix2
-rw-r--r--nixos/tests/blockbook-frontend.nix2
-rw-r--r--nixos/tests/blocky.nix34
-rw-r--r--nixos/tests/boot-stage1.nix2
-rw-r--r--nixos/tests/boot.nix1
-rw-r--r--nixos/tests/botamusique.nix4
-rw-r--r--nixos/tests/bpf.nix8
-rw-r--r--nixos/tests/breitbandmessung.nix2
-rw-r--r--nixos/tests/brscan5.nix2
-rw-r--r--nixos/tests/btrbk-no-timer.nix37
-rw-r--r--nixos/tests/buildkite-agents.nix2
-rw-r--r--nixos/tests/caddy.nix14
-rw-r--r--nixos/tests/cage.nix2
-rw-r--r--nixos/tests/cagebreak.nix2
-rw-r--r--nixos/tests/ceph-multi-node.nix2
-rw-r--r--nixos/tests/cfssl.nix2
-rw-r--r--nixos/tests/chromium.nix40
-rw-r--r--nixos/tests/clickhouse.nix2
-rw-r--r--nixos/tests/cloud-init.nix2
-rw-r--r--nixos/tests/cntr.nix22
-rw-r--r--nixos/tests/collectd.nix9
-rw-r--r--nixos/tests/common/lxd/config.yaml24
-rw-r--r--nixos/tests/containers-bridge.nix2
-rw-r--r--nixos/tests/containers-custom-pkgs.nix4
-rw-r--r--nixos/tests/containers-ephemeral.nix8
-rw-r--r--nixos/tests/containers-extra_veth.nix2
-rw-r--r--nixos/tests/containers-hosts.nix2
-rw-r--r--nixos/tests/containers-imperative.nix17
-rw-r--r--nixos/tests/containers-ip.nix2
-rw-r--r--nixos/tests/containers-names.nix2
-rw-r--r--nixos/tests/containers-nested.nix2
-rw-r--r--nixos/tests/containers-portforward.nix2
-rw-r--r--nixos/tests/containers-tmpfs.nix12
-rw-r--r--nixos/tests/convos.nix2
-rw-r--r--nixos/tests/cri-o.nix2
-rw-r--r--nixos/tests/cryptpad.nix18
-rw-r--r--nixos/tests/custom-ca.nix196
-rw-r--r--nixos/tests/deluge.nix2
-rw-r--r--nixos/tests/disable-installer-tools.nix2
-rw-r--r--nixos/tests/dnsdist.nix2
-rw-r--r--nixos/tests/doas.nix2
-rw-r--r--nixos/tests/docker-edge.nix49
-rw-r--r--nixos/tests/docker-registry.nix2
-rw-r--r--nixos/tests/docker-tools-cross.nix10
-rw-r--r--nixos/tests/docker-tools.nix7
-rw-r--r--nixos/tests/documize.nix2
-rw-r--r--nixos/tests/doh-proxy-rust.nix2
-rw-r--r--nixos/tests/domination.nix2
-rw-r--r--nixos/tests/dovecot.nix2
-rw-r--r--nixos/tests/earlyoom.nix16
-rw-r--r--nixos/tests/ecryptfs.nix12
-rw-r--r--nixos/tests/emacs-daemon.nix4
-rw-r--r--nixos/tests/empty-file0
-rw-r--r--nixos/tests/enlightenment.nix2
-rw-r--r--nixos/tests/env.nix2
-rw-r--r--nixos/tests/envoy.nix33
-rw-r--r--nixos/tests/etebase-server.nix2
-rw-r--r--nixos/tests/etesync-dav.nix2
-rw-r--r--nixos/tests/extra-python-packages.nix13
-rw-r--r--nixos/tests/fancontrol.nix2
-rw-r--r--nixos/tests/fcitx/default.nix3
-rw-r--r--nixos/tests/fenics.nix2
-rw-r--r--nixos/tests/firefox.nix27
-rw-r--r--nixos/tests/fish.nix2
-rw-r--r--nixos/tests/fluentd.nix2
-rw-r--r--nixos/tests/fontconfig-default-fonts.nix2
-rw-r--r--nixos/tests/freeswitch.nix2
-rw-r--r--nixos/tests/fsck.nix2
-rw-r--r--nixos/tests/ft2-clone.nix2
-rw-r--r--nixos/tests/geth.nix2
-rw-r--r--nixos/tests/gitlab.nix321
-rw-r--r--nixos/tests/gitolite-fcgiwrap.nix4
-rw-r--r--nixos/tests/gitolite.nix2
-rw-r--r--nixos/tests/gnome-xorg.nix3
-rw-r--r--nixos/tests/gnome.nix3
-rw-r--r--nixos/tests/gocd-agent.nix2
-rw-r--r--nixos/tests/gotify-server.nix2
-rw-r--r--nixos/tests/grafana-agent.nix32
-rw-r--r--nixos/tests/graphite.nix12
-rw-r--r--nixos/tests/graylog.nix2
-rw-r--r--nixos/tests/grocy.nix2
-rw-r--r--nixos/tests/grub.nix2
-rw-r--r--nixos/tests/hadoop/default.nix7
-rw-r--r--nixos/tests/hadoop/hadoop.nix273
-rw-r--r--nixos/tests/hadoop/hdfs.nix61
-rw-r--r--nixos/tests/hadoop/yarn.nix34
-rw-r--r--nixos/tests/hardened.nix11
-rw-r--r--nixos/tests/haste-server.nix23
-rw-r--r--nixos/tests/hbase.nix30
-rw-r--r--nixos/tests/hedgedoc.nix4
-rw-r--r--nixos/tests/herbstluftwm.nix2
-rw-r--r--nixos/tests/hibernate.nix11
-rw-r--r--nixos/tests/hitch/default.nix2
-rw-r--r--nixos/tests/hocker-fetchdocker/default.nix2
-rw-r--r--nixos/tests/hockeypuck.nix2
-rw-r--r--nixos/tests/home-assistant.nix167
-rw-r--r--nixos/tests/hostname.nix2
-rw-r--r--nixos/tests/hound.nix2
-rw-r--r--nixos/tests/hydra/default.nix4
-rw-r--r--nixos/tests/i3wm.nix2
-rw-r--r--nixos/tests/ihatemoney/default.nix11
-rw-r--r--nixos/tests/incron.nix2
-rw-r--r--nixos/tests/initrd-network.nix2
-rw-r--r--nixos/tests/initrd-secrets.nix2
-rw-r--r--nixos/tests/input-remapper.nix52
-rw-r--r--nixos/tests/installed-tests/appstream-qt.nix9
-rw-r--r--nixos/tests/installed-tests/appstream.nix9
-rw-r--r--nixos/tests/installed-tests/default.nix6
-rw-r--r--nixos/tests/installed-tests/flatpak-builder.nix1
-rw-r--r--nixos/tests/installed-tests/geocode-glib.nix13
-rw-r--r--nixos/tests/installed-tests/gjs.nix6
-rw-r--r--nixos/tests/installed-tests/power-profiles-daemon.nix9
-rw-r--r--nixos/tests/installer-systemd-stage-1.nix33
-rw-r--r--nixos/tests/installer.nix97
-rw-r--r--nixos/tests/invidious.nix2
-rw-r--r--nixos/tests/ipfs.nix22
-rw-r--r--nixos/tests/isso.nix4
-rw-r--r--nixos/tests/jellyfin.nix26
-rw-r--r--nixos/tests/jenkins.nix23
-rw-r--r--nixos/tests/jibri.nix2
-rw-r--r--nixos/tests/jitsi-meet.nix7
-rw-r--r--nixos/tests/k3s-single-node-docker.nix84
-rw-r--r--nixos/tests/k3s-single-node.nix2
-rw-r--r--nixos/tests/kanidm.nix75
-rw-r--r--nixos/tests/kbd-setfont-decompress.nix2
-rw-r--r--nixos/tests/kbd-update-search-paths-patch.nix2
-rw-r--r--nixos/tests/keepassxc.nix52
-rw-r--r--nixos/tests/kerberos/heimdal.nix2
-rw-r--r--nixos/tests/kerberos/mit.nix2
-rw-r--r--nixos/tests/kernel-generic.nix3
-rw-r--r--nixos/tests/kernel-latest-ath-user-regd.nix2
-rw-r--r--nixos/tests/kexec.nix59
-rw-r--r--nixos/tests/keycloak.nix46
-rw-r--r--nixos/tests/krb5/deprecated-config.nix2
-rw-r--r--nixos/tests/krb5/example-config.nix2
-rw-r--r--nixos/tests/ksm.nix2
-rw-r--r--nixos/tests/kubernetes/base.nix4
-rw-r--r--nixos/tests/kubernetes/dns.nix12
-rw-r--r--nixos/tests/kubernetes/rbac.nix8
-rw-r--r--nixos/tests/libinput.nix2
-rw-r--r--nixos/tests/libreddit.nix14
-rw-r--r--nixos/tests/libresprite.nix2
-rw-r--r--nixos/tests/libuiohook.nix21
-rw-r--r--nixos/tests/lidarr.nix2
-rw-r--r--nixos/tests/lightdm.nix2
-rw-r--r--nixos/tests/lighttpd.nix21
-rw-r--r--nixos/tests/limesurvey.nix2
-rw-r--r--nixos/tests/litestream.nix2
-rw-r--r--nixos/tests/login.nix8
-rw-r--r--nixos/tests/logrotate.nix152
-rw-r--r--nixos/tests/loki.nix2
-rw-r--r--nixos/tests/lorri/default.nix2
-rw-r--r--nixos/tests/lvm2/default.nix45
-rw-r--r--nixos/tests/lvm2/systemd-stage-1.nix104
-rw-r--r--nixos/tests/lvm2/thinpool.nix32
-rw-r--r--nixos/tests/lvm2/vdo.nix27
-rw-r--r--nixos/tests/lxd-image-server.nix65
-rw-r--r--nixos/tests/lxd-image.nix89
-rw-r--r--nixos/tests/lxd-nftables.nix2
-rw-r--r--nixos/tests/lxd.nix105
-rw-r--r--nixos/tests/maddy.nix2
-rw-r--r--nixos/tests/maestral.nix72
-rw-r--r--nixos/tests/magnetico.nix2
-rw-r--r--nixos/tests/mailcatcher.nix13
-rw-r--r--nixos/tests/mailhog.nix6
-rw-r--r--nixos/tests/matomo.nix2
-rw-r--r--nixos/tests/matrix/appservice-irc.nix (renamed from nixos/tests/matrix-appservice-irc.nix)54
-rw-r--r--nixos/tests/matrix/conduit.nix (renamed from nixos/tests/matrix-conduit.nix)2
-rw-r--r--nixos/tests/matrix/dendrite.nix (renamed from nixos/tests/dendrite.nix)6
-rw-r--r--nixos/tests/matrix/mjolnir.nix44
-rw-r--r--nixos/tests/matrix/pantalaimon.nix31
-rw-r--r--nixos/tests/matrix/synapse.nix (renamed from nixos/tests/matrix-synapse.nix)68
-rw-r--r--nixos/tests/mediawiki.nix2
-rw-r--r--nixos/tests/meilisearch.nix15
-rw-r--r--nixos/tests/memcached.nix2
-rw-r--r--nixos/tests/mimir.nix50
-rw-r--r--nixos/tests/minecraft-server.nix1
-rw-r--r--nixos/tests/minidlna.nix8
-rw-r--r--nixos/tests/miniflux.nix24
-rw-r--r--nixos/tests/misc.nix8
-rw-r--r--nixos/tests/mod_perl.nix2
-rw-r--r--nixos/tests/mongodb.nix4
-rw-r--r--nixos/tests/moodle.nix2
-rw-r--r--nixos/tests/moonraker.nix45
-rw-r--r--nixos/tests/mosquitto.nix28
-rw-r--r--nixos/tests/mtp.nix109
-rw-r--r--nixos/tests/musescore.nix2
-rw-r--r--nixos/tests/mysql/mysql-autobackup.nix2
-rw-r--r--nixos/tests/mysql/mysql-backup.nix1
-rw-r--r--nixos/tests/n8n.nix4
-rw-r--r--nixos/tests/nagios.nix2
-rw-r--r--nixos/tests/nano.nix44
-rw-r--r--nixos/tests/nar-serve.nix2
-rw-r--r--nixos/tests/nats.nix24
-rw-r--r--nixos/tests/navidrome.nix4
-rw-r--r--nixos/tests/nbd.nix103
-rw-r--r--nixos/tests/ncdns.nix19
-rw-r--r--nixos/tests/networking.nix159
-rw-r--r--nixos/tests/nextcloud/default.nix6
-rw-r--r--nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix118
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix23
-rw-r--r--nixos/tests/nghttpx.nix4
-rw-r--r--nixos/tests/nginx-http3.nix93
-rw-r--r--nixos/tests/nginx-modsecurity.nix39
-rw-r--r--nixos/tests/nginx-pubhtml.nix2
-rw-r--r--nixos/tests/nginx-sandbox.nix2
-rw-r--r--nixos/tests/nginx-sso.nix2
-rw-r--r--nixos/tests/nginx-variants.nix2
-rw-r--r--nixos/tests/nitter.nix2
-rw-r--r--nixos/tests/nix-ld.nix20
-rw-r--r--nixos/tests/nix-serve.nix2
-rw-r--r--nixos/tests/nixops/default.nix4
-rw-r--r--nixos/tests/nixos-generate-config.nix2
-rw-r--r--nixos/tests/node-red.nix2
-rw-r--r--nixos/tests/non-default-filesystems.nix54
-rw-r--r--nixos/tests/noto-fonts.nix2
-rw-r--r--nixos/tests/novacomd.nix2
-rw-r--r--nixos/tests/oh-my-zsh.nix2
-rw-r--r--nixos/tests/ombi.nix2
-rw-r--r--nixos/tests/openldap.nix6
-rw-r--r--nixos/tests/openssh.nix18
-rw-r--r--nixos/tests/opentabletdriver.nix2
-rw-r--r--nixos/tests/os-prober.nix25
-rw-r--r--nixos/tests/osrm-backend.nix2
-rw-r--r--nixos/tests/overlayfs.nix2
-rw-r--r--nixos/tests/pacemaker.nix110
-rw-r--r--nixos/tests/packagekit.nix2
-rw-r--r--nixos/tests/pam/pam-oath-login.nix26
-rw-r--r--nixos/tests/pam/pam-u2f.nix5
-rw-r--r--nixos/tests/pam/pam-ussh.nix70
-rw-r--r--nixos/tests/pantheon.nix2
-rw-r--r--nixos/tests/paperless.nix (renamed from nixos/tests/paperless-ng.nix)25
-rw-r--r--nixos/tests/pass-secret-service.nix69
-rw-r--r--nixos/tests/pdns-recursor.nix7
-rw-r--r--nixos/tests/pgadmin4-standalone.nix43
-rw-r--r--nixos/tests/pgadmin4.nix136
-rw-r--r--nixos/tests/php/fpm.nix4
-rw-r--r--nixos/tests/php/httpd.nix2
-rw-r--r--nixos/tests/php/pcre.nix2
-rw-r--r--nixos/tests/pict-rs.nix4
-rw-r--r--nixos/tests/plasma5-systemd-start.nix2
-rw-r--r--nixos/tests/plasma5.nix10
-rw-r--r--nixos/tests/plausible.nix2
-rw-r--r--nixos/tests/pleroma.nix24
-rw-r--r--nixos/tests/plikd.nix4
-rw-r--r--nixos/tests/plotinus.nix2
-rw-r--r--nixos/tests/podgrab.nix4
-rw-r--r--nixos/tests/podman/default.nix2
-rw-r--r--nixos/tests/podman/tls-ghostunnel.nix2
-rw-r--r--nixos/tests/polaris.nix31
-rw-r--r--nixos/tests/postfix-raise-smtpd-tls-security-level.nix2
-rw-r--r--nixos/tests/postfix.nix2
-rw-r--r--nixos/tests/postgresql-wal-receiver.nix2
-rw-r--r--nixos/tests/postgresql.nix2
-rw-r--r--nixos/tests/power-profiles-daemon.nix2
-rw-r--r--nixos/tests/powerdns.nix1
-rw-r--r--nixos/tests/predictable-interface-names.nix2
-rw-r--r--nixos/tests/privacyidea.nix2
-rw-r--r--nixos/tests/privoxy.nix10
-rw-r--r--nixos/tests/prometheus-exporters.nix44
-rw-r--r--nixos/tests/prowlarr.nix2
-rw-r--r--nixos/tests/pt2-clone.nix2
-rw-r--r--nixos/tests/public-inbox.nix227
-rw-r--r--nixos/tests/pulseaudio.nix2
-rw-r--r--nixos/tests/pykms.nix14
-rw-r--r--nixos/tests/qboot.nix2
-rw-r--r--nixos/tests/rabbitmq.nix4
-rw-r--r--nixos/tests/radarr.nix2
-rw-r--r--nixos/tests/radicale.nix2
-rw-r--r--nixos/tests/rasdaemon.nix2
-rw-r--r--nixos/tests/redis.nix2
-rw-r--r--nixos/tests/redmine.nix2
-rw-r--r--nixos/tests/restart-by-activation-script.nix2
-rw-r--r--nixos/tests/restic.nix191
-rw-r--r--nixos/tests/retroarch.nix2
-rw-r--r--nixos/tests/riak.nix18
-rw-r--r--nixos/tests/rspamd.nix10
-rw-r--r--nixos/tests/rstudio-server.nix6
-rw-r--r--nixos/tests/rsyslogd.nix4
-rw-r--r--nixos/tests/sabnzbd.nix2
-rw-r--r--nixos/tests/schleuder.nix128
-rw-r--r--nixos/tests/sddm.nix4
-rw-r--r--nixos/tests/sfxr-qt.nix32
-rw-r--r--nixos/tests/shadow.nix24
-rw-r--r--nixos/tests/shattered-pixel-dungeon.nix2
-rw-r--r--nixos/tests/shiori.nix3
-rw-r--r--nixos/tests/signal-desktop.nix4
-rw-r--r--nixos/tests/simple.nix2
-rw-r--r--nixos/tests/snapcast.nix1
-rw-r--r--nixos/tests/snapper.nix2
-rw-r--r--nixos/tests/soapui.nix2
-rw-r--r--nixos/tests/solr.nix2
-rw-r--r--nixos/tests/sonarr.nix2
-rw-r--r--nixos/tests/sourcehut.nix63
-rw-r--r--nixos/tests/sssd-ldap.nix2
-rw-r--r--nixos/tests/sssd.nix2
-rw-r--r--nixos/tests/starship.nix2
-rw-r--r--nixos/tests/step-ca.nix5
-rw-r--r--nixos/tests/swap-partition.nix48
-rw-r--r--nixos/tests/sway.nix8
-rw-r--r--nixos/tests/switch-test.nix628
-rw-r--r--nixos/tests/sympa.nix2
-rw-r--r--nixos/tests/syncthing-init.nix2
-rw-r--r--nixos/tests/syncthing-relay.nix2
-rw-r--r--nixos/tests/systemd-analyze.nix2
-rw-r--r--nixos/tests/systemd-binfmt.nix6
-rw-r--r--nixos/tests/systemd-boot.nix18
-rw-r--r--nixos/tests/systemd-confinement.nix31
-rw-r--r--nixos/tests/systemd-cryptenroll.nix2
-rw-r--r--nixos/tests/systemd-escaping.nix45
-rw-r--r--nixos/tests/systemd-initrd-btrfs-raid.nix45
-rw-r--r--nixos/tests/systemd-initrd-luks-keyfile.nix53
-rw-r--r--nixos/tests/systemd-initrd-luks-password.nix48
-rw-r--r--nixos/tests/systemd-initrd-simple.nix44
-rw-r--r--nixos/tests/systemd-initrd-swraid.nix50
-rw-r--r--nixos/tests/systemd-journal.nix2
-rw-r--r--nixos/tests/systemd-machinectl.nix85
-rw-r--r--nixos/tests/systemd-misc.nix (renamed from nixos/tests/systemd-unit-path.nix)19
-rw-r--r--nixos/tests/systemd-networkd-vrf.nix65
-rw-r--r--nixos/tests/systemd-networkd.nix10
-rw-r--r--nixos/tests/systemd-nspawn.nix13
-rw-r--r--nixos/tests/systemd-shutdown.nix26
-rw-r--r--nixos/tests/systemd.nix6
-rw-r--r--nixos/tests/taskserver.nix1
-rw-r--r--nixos/tests/telegraf.nix2
-rw-r--r--nixos/tests/teleport.nix12
-rw-r--r--nixos/tests/terminal-emulators.nix208
-rw-r--r--nixos/tests/tinywl.nix3
-rw-r--r--nixos/tests/tomcat.nix21
-rw-r--r--nixos/tests/tor.nix25
-rw-r--r--nixos/tests/traefik.nix22
-rw-r--r--nixos/tests/transmission.nix2
-rw-r--r--nixos/tests/tsm-client-gui.nix2
-rw-r--r--nixos/tests/tuptime.nix2
-rw-r--r--nixos/tests/turbovnc-headless-server.nix2
-rw-r--r--nixos/tests/tuxguitar.nix2
-rw-r--r--nixos/tests/udisks2.nix2
-rw-r--r--nixos/tests/uptermd.nix65
-rw-r--r--nixos/tests/usbguard.nix2
-rw-r--r--nixos/tests/user-activation-scripts.nix6
-rw-r--r--nixos/tests/user-home-mode.nix27
-rw-r--r--nixos/tests/uwsgi.nix2
-rw-r--r--nixos/tests/v2ray.nix2
-rw-r--r--nixos/tests/vault-dev.nix35
-rw-r--r--nixos/tests/vault-postgresql.nix2
-rw-r--r--nixos/tests/vault.nix2
-rw-r--r--nixos/tests/vaultwarden.nix1
-rw-r--r--nixos/tests/vector.nix2
-rw-r--r--nixos/tests/vengi-tools.nix4
-rw-r--r--nixos/tests/vikunja.nix4
-rw-r--r--nixos/tests/virtualbox.nix33
-rw-r--r--nixos/tests/vscodium.nix8
-rw-r--r--nixos/tests/vsftpd.nix42
-rw-r--r--nixos/tests/web-apps/healthchecks.nix42
-rw-r--r--nixos/tests/web-apps/mastodon.nix170
-rw-r--r--nixos/tests/web-apps/netbox.nix30
-rw-r--r--nixos/tests/web-apps/nifi.nix30
-rw-r--r--nixos/tests/web-apps/peertube.nix13
-rw-r--r--nixos/tests/web-apps/phylactery.nix20
-rw-r--r--nixos/tests/web-servers/agate.nix29
-rw-r--r--nixos/tests/web-servers/unit-php.nix19
-rw-r--r--nixos/tests/wiki-js.nix2
-rw-r--r--nixos/tests/wine.nix15
-rw-r--r--nixos/tests/wireguard/wg-quick.nix5
-rw-r--r--nixos/tests/without-nix.nix21
-rw-r--r--nixos/tests/wmderland.nix2
-rw-r--r--nixos/tests/wpa_supplicant.nix2
-rw-r--r--nixos/tests/xfce.nix2
-rw-r--r--nixos/tests/xmonad-xdg-autostart.nix35
-rw-r--r--nixos/tests/xmonad.nix17
-rw-r--r--nixos/tests/xmpp/prosody-mysql.nix124
-rw-r--r--nixos/tests/xmpp/prosody.nix1
-rw-r--r--nixos/tests/xmpp/xmpp-sendmessage.nix5
-rw-r--r--nixos/tests/xrdp.nix2
-rw-r--r--nixos/tests/xterm.nix2
-rw-r--r--nixos/tests/yabar.nix2
-rw-r--r--nixos/tests/zammad.nix60
-rw-r--r--nixos/tests/zeronet-conservancy.nix25
-rw-r--r--nixos/tests/zfs.nix52
-rw-r--r--nixos/tests/zigbee2mqtt.nix2
-rw-r--r--nixos/tests/zoneminder.nix2
-rw-r--r--nixos/tests/zrepl.nix66
-rw-r--r--nixos/tests/zsh-history.nix6
997 files changed, 34332 insertions, 13261 deletions
diff --git a/nixos/doc/manual/administration/declarative-containers.section.md b/nixos/doc/manual/administration/declarative-containers.section.md
index 0d9d4017ed81b..00fd244bb91fb 100644
--- a/nixos/doc/manual/administration/declarative-containers.section.md
+++ b/nixos/doc/manual/administration/declarative-containers.section.md
@@ -40,7 +40,7 @@ section for details on container networking.)
 To disable the container, just remove it from `configuration.nix` and
 run `nixos-rebuild
   switch`. Note that this will not delete the root directory of the
-container in `/var/lib/containers`. Containers can be destroyed using
+container in `/var/lib/nixos-containers`. Containers can be destroyed using
 the imperative method: `nixos-container destroy foo`.
 
 Declarative containers can be started and stopped using the
diff --git a/nixos/doc/manual/administration/imperative-containers.section.md b/nixos/doc/manual/administration/imperative-containers.section.md
index 05196bf5d819e..f45991780c4b9 100644
--- a/nixos/doc/manual/administration/imperative-containers.section.md
+++ b/nixos/doc/manual/administration/imperative-containers.section.md
@@ -10,8 +10,8 @@ You create a container with identifier `foo` as follows:
 # nixos-container create foo
 ```
 
-This creates the container's root directory in `/var/lib/containers/foo`
-and a small configuration file in `/etc/containers/foo.conf`. It also
+This creates the container's root directory in `/var/lib/nixos-containers/foo`
+and a small configuration file in `/etc/nixos-containers/foo.conf`. It also
 builds the container's initial system configuration and stores it in
 `/nix/var/nix/profiles/per-container/foo/system`. You can modify the
 initial configuration of the container on the command line. For
diff --git a/nixos/doc/manual/configuration/adding-custom-packages.section.md b/nixos/doc/manual/configuration/adding-custom-packages.section.md
index 5d1198fb0f418..9219396722f03 100644
--- a/nixos/doc/manual/configuration/adding-custom-packages.section.md
+++ b/nixos/doc/manual/configuration/adding-custom-packages.section.md
@@ -1,11 +1,18 @@
 # Adding Custom Packages {#sec-custom-packages}
 
 It's possible that a package you need is not available in NixOS. In that
-case, you can do two things. First, you can clone the Nixpkgs
-repository, add the package to your clone, and (optionally) submit a
-patch or pull request to have it accepted into the main Nixpkgs repository.
-This is described in detail in the [Nixpkgs manual](https://nixos.org/nixpkgs/manual).
-In short, you clone Nixpkgs:
+case, you can do two things. Either you can package it with Nix, or you can try
+to use prebuilt packages from upstream. Due to the peculiarities of NixOS, it
+is important to note that building software from source is often easier than
+using pre-built executables.
+
+## Building with Nix {#sec-custom-packages-nix}
+
+This can be done either in-tree or out-of-tree. For an in-tree build, you can
+clone the Nixpkgs repository, add the package to your clone, and (optionally)
+submit a patch or pull request to have it accepted into the main Nixpkgs
+repository. This is described in detail in the [Nixpkgs
+manual](https://nixos.org/nixpkgs/manual). In short, you clone Nixpkgs:
 
 ```ShellSession
 $ git clone https://github.com/NixOS/nixpkgs
@@ -72,3 +79,21 @@ $ nix-build my-hello.nix
 $ ./result/bin/hello
 Hello, world!
 ```
+
+## Using pre-built executables {#sec-custom-packages-prebuilt}
+
+Most pre-built executables will not work on NixOS. There are two notable
+exceptions: flatpaks and AppImages. For flatpaks see the [dedicated
+section](#module-services-flatpak). AppImages will not run "as-is" on NixOS.
+First you need to install `appimage-run`: add to `/etc/nixos/configuration.nix`
+
+```nix
+environment.systemPackages = [ pkgs.appimage-run ];
+```
+
+Then instead of running the AppImage "as-is", run `appimage-run foo.appimage`.
+
+To make other pre-built executables work on NixOS, you need to package them
+with Nix and special helpers like `autoPatchelfHook` or `buildFHSUserEnv`. See
+the [Nixpkgs manual](https://nixos.org/nixpkgs/manual) for details. This
+is complex and often doing a source build is easier.
diff --git a/nixos/doc/manual/configuration/xfce.chapter.md b/nixos/doc/manual/configuration/xfce.chapter.md
index b0ef6682aae86..ee60d465e3b30 100644
--- a/nixos/doc/manual/configuration/xfce.chapter.md
+++ b/nixos/doc/manual/configuration/xfce.chapter.md
@@ -24,11 +24,16 @@ Some Xfce programs are not installed automatically. To install them
 manually (system wide), put them into your
 [](#opt-environment.systemPackages) from `pkgs.xfce`.
 
-## Thunar Plugins {#sec-xfce-thunar-plugins .unnumbered}
+## Thunar {#sec-xfce-thunar-plugins .unnumbered}
+
+Thunar (the Xfce file manager) is automatically enabled when Xfce is
+enabled. To enable Thunar without enabling Xfce, use the configuration
+option [](#opt-programs.thunar.enable) instead of simply adding
+`pkgs.xfce.thunar` to [](#opt-environment.systemPackages).
 
 If you\'d like to add extra plugins to Thunar, add them to
-[](#opt-services.xserver.desktopManager.xfce.thunarPlugins).
-You shouldn\'t just add them to [](#opt-environment.systemPackages).
+[](#opt-programs.thunar.plugins). You shouldn\'t just add them to
+[](#opt-environment.systemPackages).
 
 ## Troubleshooting {#sec-xfce-troubleshooting .unnumbered}
 
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index e96bc47b4a53b..1cd769b6a544d 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -14,6 +14,10 @@ with pkgs;
 let
   lib = pkgs.lib;
 
+  docbook_xsl_ns = pkgs.docbook-xsl-ns.override {
+    withManOptDedupPatch = true;
+  };
+
   # We need to strip references to /nix/store/* from options,
   # including any `extraSources` if some modules came from elsewhere,
   # or else the build will fail.
@@ -129,12 +133,12 @@ let
           # ^ redirect assumes xmllint doesn’t print to stdout
       }
 
-      lintrng manual-combined.xml
-      lintrng man-pages-combined.xml
-
       mkdir $out
       cp manual-combined.xml $out/
       cp man-pages-combined.xml $out/
+
+      lintrng $out/manual-combined.xml
+      lintrng $out/man-pages-combined.xml
     '';
 
   olinkDB = runCommand "manual-olinkdb"
diff --git a/nixos/doc/manual/development/activation-script.section.md b/nixos/doc/manual/development/activation-script.section.md
new file mode 100644
index 0000000000000..1aee252fddea1
--- /dev/null
+++ b/nixos/doc/manual/development/activation-script.section.md
@@ -0,0 +1,72 @@
+# Activation script {#sec-activation-script}
+
+The activation script is a bash script called to activate the new
+configuration which resides in a NixOS system in `$out/activate`. Since its
+contents depend on your system configuration, the contents may differ.
+This chapter explains how the script works in general and some common NixOS
+snippets. Please be aware that the script is executed on every boot and system
+switch, so tasks that can be performed in other places should be performed
+there (for example letting a directory of a service be created by systemd using
+mechanisms like `StateDirectory`, `CacheDirectory`, ... or if that's not
+possible using `preStart` of the service).
+
+Activation scripts are defined as snippets using
+[](#opt-system.activationScripts). They can either be a simple multiline string
+or an attribute set that can depend on other snippets. The builder for the
+activation script will take these dependencies into account and order the
+snippets accordingly. As a simple example:
+
+```nix
+system.activationScripts.my-activation-script = {
+  deps = [ "etc" ];
+  # supportsDryActivation = true;
+  text = ''
+    echo "Hallo i bims"
+  '';
+};
+```
+
+This example creates an activation script snippet that is run after the `etc`
+snippet. The special variable `supportsDryActivation` can be set so the snippet
+is also run when `nixos-rebuild dry-activate` is run. To differentiate between
+real and dry activation, the `$NIXOS_ACTION` environment variable can be
+read which is set to `dry-activate` when a dry activation is done.
+
+An activation script can write to special files instructing
+`switch-to-configuration` to restart/reload units. The script will take these
+requests into account and will incorperate the unit configuration as described
+above. This means that the activation script will "fake" a modified unit file
+and `switch-to-configuration` will act accordingly. By doing so, configuration
+like [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services) is
+respected. Since the activation script is run **after** services are already
+stopped, [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)
+cannot be taken into account anymore and the unit is always restarted instead
+of being stopped and started afterwards.
+
+The files that can be written to are `/run/nixos/activation-restart-list` and
+`/run/nixos/activation-reload-list` with their respective counterparts for
+dry activation being `/run/nixos/dry-activation-restart-list` and
+`/run/nixos/dry-activation-reload-list`. Those files can contain
+newline-separated lists of unit names where duplicates are being ignored. These
+files are not create automatically and activation scripts must take the
+possiblility into account that they have to create them first.
+
+## NixOS snippets {#sec-activation-script-nixos-snippets}
+
+There are some snippets NixOS enables by default because disabling them would
+most likely break your system. This section lists a few of them and what they
+do:
+
+- `binsh` creates `/bin/sh` which points to the runtime shell
+- `etc` sets up the contents of `/etc`, this includes systemd units and
+  excludes `/etc/passwd`, `/etc/group`, and `/etc/shadow` (which are managed by
+  the `users` snippet)
+- `hostname` sets the system's hostname in the kernel (not in `/etc`)
+- `modprobe` sets the path to the `modprobe` binary for module auto-loading
+- `nix` prepares the nix store and adds a default initial channel
+- `specialfs` is responsible for mounting filesystems like `/proc` and `sys`
+- `users` creates and removes users and groups by managing `/etc/passwd`,
+  `/etc/group` and `/etc/shadow`. This also creates home directories
+- `usrbinenv` creates `/usr/bin/env`
+- `var` creates some directories in `/var` that are not service-specific
+- `wrappers` creates setuid wrappers like `ping` and `sudo`
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
index 0b2ad60a878b0..624ee3931659b 100644
--- a/nixos/doc/manual/development/development.xml
+++ b/nixos/doc/manual/development/development.xml
@@ -12,8 +12,8 @@
  <xi:include href="../from_md/development/sources.chapter.xml" />
  <xi:include href="../from_md/development/writing-modules.chapter.xml" />
  <xi:include href="../from_md/development/building-parts.chapter.xml" />
+ <xi:include href="../from_md/development/what-happens-during-a-system-switch.chapter.xml" />
  <xi:include href="../from_md/development/writing-documentation.chapter.xml" />
- <xi:include href="../from_md/development/building-nixos.chapter.xml" />
  <xi:include href="../from_md/development/nixos-tests.chapter.xml" />
  <xi:include href="../from_md/development/testing-installer.chapter.xml" />
 </part>
diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md
index fff06e1ea5ba7..79914f2cb6ca9 100644
--- a/nixos/doc/manual/development/option-declarations.section.md
+++ b/nixos/doc/manual/development/option-declarations.section.md
@@ -27,9 +27,10 @@ The function `mkOption` accepts the following arguments.
 
 `type`
 
-:   The type of the option (see [](#sec-option-types)). It may be
-    omitted, but that's not advisable since it may lead to errors that
-    are hard to diagnose.
+:   The type of the option (see [](#sec-option-types)). This
+    argument is mandatory for nixpkgs modules. Setting this is highly
+    recommended for the sake of documentation and type checking. In case it is
+    not set, a fallback type with unspecified behavior is used.
 
 `default`
 
@@ -55,7 +56,14 @@ The function `mkOption` accepts the following arguments.
 `description`
 
 :   A textual description of the option, in DocBook format, that will be
-    included in the NixOS manual.
+    included in the NixOS manual. During the migration process from DocBook
+    to CommonMark the description may also be written in CommonMark, but has
+    to be wrapped in `lib.mdDoc` to differentiate it from DocBook. See
+    the nixpkgs manual for [the list of CommonMark extensions](
+    https://nixos.org/nixpkgs/manual/#sec-contributing-markup)
+    supported by NixOS documentation.
+
+    New documentation should preferably be written as CommonMark.
 
 ## Utility functions for common option patterns {#sec-option-declarations-util}
 
@@ -119,14 +127,14 @@ lib.mkOption {
 ```nix
 lib.mkPackageOption pkgs "GHC" {
   default = [ "ghc" ];
-  example = "pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])";
+  example = "pkgs.haskell.package.ghc923.ghc.withPackages (hkgs: [ hkgs.primes ])";
 }
 # is like
 lib.mkOption {
   type = lib.types.package;
   default = pkgs.ghc;
   defaultText = lib.literalExpression "pkgs.ghc";
-  example = lib.literalExpression "pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])";
+  example = lib.literalExpression "pkgs.haskell.package.ghc923.ghc.withPackages (hkgs: [ hkgs.primes ])";
   description = "The GHC package to use.";
 }
 ```
@@ -145,26 +153,26 @@ As an example, we will take the case of display managers. There is a
 central display manager module for generic display manager options and a
 module file per display manager backend (sddm, gdm \...).
 
-There are two approach to this module structure:
+There are two approaches we could take with this module structure:
 
--   Managing the display managers independently by adding an enable
+-   Configuring the display managers independently by adding an enable
     option to every display manager module backend. (NixOS)
 
--   Managing the display managers in the central module by adding an
-    option to select which display manager backend to use.
+-   Configuring the display managers in the central module by adding
+    an option to select which display manager backend to use.
 
 Both approaches have problems.
 
 Making backends independent can quickly become hard to manage. For
-display managers, there can be only one enabled at a time, but the type
-system can not enforce this restriction as there is no relation between
-each backend `enable` option. As a result, this restriction has to be
-done explicitely by adding assertions in each display manager backend
-module.
+display managers, there can only be one enabled at a time, but the
+type system cannot enforce this restriction as there is no relation
+between each backend's `enable` option. As a result, this restriction
+has to be done explicitly by adding assertions in each display manager
+backend module.
 
-On the other hand, managing the display managers backends in the central
-module will require to change the central module option every time a new
-backend is added or removed.
+On the other hand, managing the display manager backends in the
+central module will require changing the central module option every
+time a new backend is added or removed.
 
 By using extensible option types, it is possible to create a placeholder
 option in the central module
@@ -175,7 +183,7 @@ and to extend it in each backend module
 
 As a result, `displayManager.enable` option values can be added without
 changing the main service module file and the type system automatically
-enforce that there can only be a single display manager enabled.
+enforces that there can only be a single display manager enabled.
 
 ::: {#ex-option-declaration-eot-service .example}
 ::: {.title}
diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md
index 56ffa8e9d79c4..9b35e66301447 100644
--- a/nixos/doc/manual/development/option-types.section.md
+++ b/nixos/doc/manual/development/option-types.section.md
@@ -16,13 +16,14 @@ merging is handled.
 
 `types.path`
 
-:   A filesystem path, defined as anything that when coerced to a string
-    starts with a slash. Even if derivations can be considered as path,
-    the more specific `types.package` should be preferred.
+:   A filesystem path is anything that starts with a slash when
+    coerced to a string. Even if derivations can be considered as
+    paths, the more specific `types.package` should be preferred.
 
 `types.package`
 
-:   A derivation or a store path.
+:   A top-level store path. This can be an attribute set pointing
+    to a store path, like a derivation or a flake input.
 
 `types.anything`
 
@@ -63,6 +64,24 @@ merging is handled.
     ```
     :::
 
+`types.raw`
+
+:   A type which doesn't do any checking, merging or nested evaluation. It
+    accepts a single arbitrary value that is not recursed into, making it
+    useful for values coming from outside the module system, such as package
+    sets or arbitrary data. Options of this type are still evaluated according
+    to priorities and conditionals, so `mkForce`, `mkIf` and co. still work on
+    the option value itself, but not for any value nested within it. This type
+    should only be used when checking, merging and nested evaluation are not
+    desirable.
+
+`types.optionType`
+
+:   The type of an option's type. Its merging operation ensures that nested
+    options have the correct file location annotated, and that if possible,
+    multiple option definitions are correctly merged together. The main use
+    case is as the type of the `_module.freeformType` option.
+
 `types.attrs`
 
 :   A free-form attribute set.
@@ -201,6 +220,25 @@ Value types are types that take a value parameter.
         requires using a function:
         `the-submodule = { ... }: { options = { ... }; }`.
 
+`types.deferredModule`
+
+:   Whereas `submodule` represents an option tree, `deferredModule` represents
+    a module value, such as a module file or a configuration.
+
+    It can be set multiple times.
+
+    Module authors can use its value in `imports`, in `submoduleWith`'s `modules`
+    or in `evalModules`' `modules` parameter, among other places.
+
+    Note that `imports` must be evaluated before the module fixpoint. Because
+    of this, deferred modules can only be imported into "other" fixpoints, such
+    as submodules.
+
+    One use case for this type is the type of a "default" module that allow the
+    user to affect all submodules in an `attrsOf submodule` at once. This is
+    more convenient and discoverable than expecting the module user to
+    type-merge with the `attrsOf submodule` option.
+
 ## Composed Types {#sec-option-types-composed}
 
 Composed types are types that take a type as parameter. `listOf
@@ -289,6 +327,10 @@ The option set can be defined directly
 ([Example: Directly defined submodule](#ex-submodule-direct)) or as reference
 ([Example: Submodule defined as a reference](#ex-submodule-reference)).
 
+Note that even if your submodule’s options all have a default value,
+you will still need to provide a default value (e.g. an empty attribute set)
+if you want to allow users to leave it undefined.
+
 ::: {#ex-submodule-direct .example}
 ::: {.title}
 **Example: Directly defined submodule**
diff --git a/nixos/doc/manual/development/running-nixos-tests.section.md b/nixos/doc/manual/development/running-nixos-tests.section.md
index d6a456f01883a..1bec023b613aa 100644
--- a/nixos/doc/manual/development/running-nixos-tests.section.md
+++ b/nixos/doc/manual/development/running-nixos-tests.section.md
@@ -24,8 +24,8 @@ After building/downloading all required dependencies, this will perform
 a build that starts a QEMU/KVM virtual machine containing a NixOS
 system. The virtual machine mounts the Nix store of the host; this makes
 VM creation very fast, as no disk image needs to be created. Afterwards,
-you can view a pretty-printed log of the test:
+you can view a log of the test:
 
 ```ShellSession
-$ firefox result/log.html
+$ nix-store --read-log result
 ```
diff --git a/nixos/doc/manual/development/settings-options.section.md b/nixos/doc/manual/development/settings-options.section.md
index 58a3d8448af5b..d569e23adbdcb 100644
--- a/nixos/doc/manual/development/settings-options.section.md
+++ b/nixos/doc/manual/development/settings-options.section.md
@@ -32,6 +32,20 @@ type of this option should represent the format. The most common formats
 have a predefined type and string generator already declared under
 `pkgs.formats`:
 
+`pkgs.formats.javaProperties` { *`comment`* ? `"Generated with Nix"` }
+
+:   A function taking an attribute set with values
+
+    `comment`
+
+    :   A string to put at the start of the
+        file in a comment. It can have multiple
+        lines.
+
+    It returns the `type`: `attrsOf str` and a function
+    `generate` to build a Java `.properties` file, taking
+    care of the correct escaping, etc.
+
 `pkgs.formats.json` { }
 
 :   A function taking an empty attribute set (for future extensibility)
@@ -66,6 +80,45 @@ have a predefined type and string generator already declared under
     and returning a set with TOML-specific attributes `type` and
     `generate` as specified [below](#pkgs-formats-result).
 
+`pkgs.formats.elixirConf { elixir ? pkgs.elixir }`
+
+:   A function taking an attribute set with values
+
+    `elixir`
+
+    :   The Elixir package which will be used to format the generated output
+
+    It returns a set with Elixir-Config-specific attributes `type`, `lib`, and
+    `generate` as specified [below](#pkgs-formats-result).
+
+    The `lib` attribute contains functions to be used in settings, for
+    generating special Elixir values:
+
+    `mkRaw elixirCode`
+
+    :   Outputs the given string as raw Elixir code
+
+    `mkGetEnv { envVariable, fallback ? null }`
+
+    :   Makes the configuration fetch an environment variable at runtime
+
+    `mkAtom atom`
+
+    :   Outputs the given string as an Elixir atom, instead of the default
+        Elixir binary string. Note: lowercase atoms still needs to be prefixed
+        with `:`
+
+    `mkTuple array`
+
+    :   Outputs the given array as an Elixir tuple, instead of the default
+        Elixir list
+
+    `mkMap attrset`
+
+    :   Outputs the given attribute set as an Elixir map, instead of the
+        default Elixir keyword list
+
+
 ::: {#pkgs-formats-result}
 These functions all return an attribute set with these values:
 :::
@@ -74,6 +127,12 @@ These functions all return an attribute set with these values:
 
 :   A module system type representing a value of the format
 
+`lib`
+
+:   Utility functions for convenience, or special interactions with the format.
+    This attribute is optional. It may contain inside a `types` attribute
+    containing types specific to this format.
+
 `generate` *`filename jsonValue`*
 
 :   A function that can render a value of the format to a file. Returns
diff --git a/nixos/doc/manual/development/unit-handling.section.md b/nixos/doc/manual/development/unit-handling.section.md
new file mode 100644
index 0000000000000..a7ccb3dbd042d
--- /dev/null
+++ b/nixos/doc/manual/development/unit-handling.section.md
@@ -0,0 +1,62 @@
+# Unit handling {#sec-unit-handling}
+
+To figure out what units need to be started/stopped/restarted/reloaded, the
+script first checks the current state of the system, similar to what `systemctl
+list-units` shows. For each of the units, the script goes through the following
+checks:
+
+- Is the unit file still in the new system? If not, **stop** the service unless
+  it sets `X-StopOnRemoval` in the `[Unit]` section to `false`.
+
+- Is it a `.target` unit? If so, **start** it unless it sets
+  `RefuseManualStart` in the `[Unit]` section to `true` or `X-OnlyManualStart`
+  in the `[Unit]` section to `true`. Also **stop** the unit again unless it
+  sets `X-StopOnReconfiguration` to `false`.
+
+- Are the contents of the unit files different? They are compared by parsing
+  them and comparing their contents. If they are different but only
+  `X-Reload-Triggers` in the `[Unit]` section is changed, **reload** the unit.
+  The NixOS module system allows setting these triggers with the option
+  [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services). There are
+  some additional keys in the `[Unit]` section that are ignored as well. If the
+  unit files differ in any way, the following actions are performed:
+
+  - `.path` and `.slice` units are ignored. There is no need to restart them
+    since changes in their values are applied by systemd when systemd is
+    reloaded.
+
+  - `.mount` units are **reload**ed. These mostly come from the `/etc/fstab`
+    parser.
+
+  - `.socket` units are currently ignored. This is to be fixed at a later
+    point.
+
+  - The rest of the units (mostly `.service` units) are then **reload**ed if
+    `X-ReloadIfChanged` in the `[Service]` section is set to `true` (exposed
+    via [systemd.services.\<name\>.reloadIfChanged](#opt-systemd.services)).
+    A little exception is done for units that were deactivated in the meantime,
+    for example because they require a unit that got stopped before. These
+    are **start**ed instead of reloaded.
+
+  - If the reload flag is not set, some more flags decide if the unit is
+    skipped. These flags are `X-RestartIfChanged` in the `[Service]` section
+    (exposed via
+    [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services)),
+    `RefuseManualStop` in the `[Unit]` section, and `X-OnlyManualStart` in the
+    `[Unit]` section.
+
+  - Further behavior depends on the unit having `X-StopIfChanged` in the
+    `[Service]` section set to `true` (exposed via
+    [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)). This is
+    set to `true` by default and must be explicitly turned off if not wanted.
+    If the flag is enabled, the unit is **stop**ped and then **start**ed. If
+    not, the unit is **restart**ed. The goal of the flag is to make sure that
+    the new unit never runs in the old environment which is still in place
+    before the activation script is run. This behavior is different when the
+    service is socket-activated, as outlined in the following steps.
+
+  - The last thing that is taken into account is whether the unit is a service
+    and socket-activated. If `X-StopIfChanged` is **not** set, the service
+    is **restart**ed with the others. If it is set, both the service and the
+    socket are **stop**ped and the socket is **start**ed, leaving socket
+    activation to start the service when it's needed.
diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
new file mode 100644
index 0000000000000..aad82831a3c24
--- /dev/null
+++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
@@ -0,0 +1,53 @@
+# What happens during a system switch? {#sec-switching-systems}
+
+Running `nixos-rebuild switch` is one of the more common tasks under NixOS.
+This chapter explains some of the internals of this command to make it simpler
+for new module developers to configure their units correctly and to make it
+easier to understand what is happening and why for curious administrators.
+
+`nixos-rebuild`, like many deployment solutions, calls `switch-to-configuration`
+which resides in a NixOS system at `$out/bin/switch-to-configuration`. The
+script is called with the action that is to be performed like `switch`, `test`,
+`boot`. There is also the `dry-activate` action which does not really perform
+the actions but rather prints what it would do if you called it with `test`.
+This feature can be used to check what service states would be changed if the
+configuration was switched to.
+
+If the action is `switch` or `boot`, the bootloader is updated first so the
+configuration will be the next one to boot. Unless `NIXOS_NO_SYNC` is set to
+`1`, `/nix/store` is synced to disk.
+
+If the action is `switch` or `test`, the currently running system is inspected
+and the actions to switch to the new system are calculated. This process takes
+two data sources into account: `/etc/fstab` and the current systemd status.
+Mounts and swaps are read from `/etc/fstab` and the corresponding actions are
+generated. If a new mount is added, for example, the proper `.mount` unit is
+marked to be started. The current systemd state is inspected, the difference
+between the current system and the desired configuration is calculated and
+actions are generated to get to this state. There are a lot of nuances that can
+be controlled by the units which are explained here.
+
+After calculating what should be done, the actions are carried out. The order
+of actions is always the same:
+- Stop units (`systemctl stop`)
+- Run activation script (`$out/activate`)
+- See if the activation script requested more units to restart
+- Restart systemd if needed (`systemd daemon-reexec`)
+- Forget about the failed state of units (`systemctl reset-failed`)
+- Reload systemd (`systemctl daemon-reload`)
+- Reload systemd user instances (`systemctl --user daemon-reload`)
+- Set up tmpfiles (`systemd-tmpfiles --create`)
+- Reload units (`systemctl reload`)
+- Restart units (`systemctl restart`)
+- Start units (`systemctl start`)
+- Inspect what changed during these actions and print units that failed and
+  that were newly started
+
+Most of these actions are either self-explaining but some of them have to do
+with our units or the activation script. For this reason, these topics are
+explained in the next sections.
+
+```{=docbook}
+<xi:include href="unit-handling.section.xml" />
+<xi:include href="activation-script.section.xml" />
+```
diff --git a/nixos/doc/manual/development/writing-modules.chapter.md b/nixos/doc/manual/development/writing-modules.chapter.md
index 2e3c6b34f1f59..0c41cbd3cb757 100644
--- a/nixos/doc/manual/development/writing-modules.chapter.md
+++ b/nixos/doc/manual/development/writing-modules.chapter.md
@@ -90,6 +90,17 @@ modules: `systemd.services` (the set of all systemd services) and
 `systemd.timers` (the list of commands to be executed periodically by
 `systemd`).
 
+Care must be taken when writing systemd services using `Exec*` directives. By
+default systemd performs substitution on `%<char>` specifiers in these
+directives, expands environment variables from `$FOO` and `${FOO}`, splits
+arguments on whitespace, and splits commands on `;`. All of these must be escaped
+to avoid unexpected substitution or splitting when interpolating into an `Exec*`
+directive, e.g. when using an `extraArgs` option to pass additional arguments to
+the service. The functions `utils.escapeSystemdExecArg` and
+`utils.escapeSystemdExecArgs` are provided for this, see [Example: Escaping in
+Exec directives](#exec-escaping-example) for an example. When using these
+functions system environment substitution should *not* be disabled explicitly.
+
 ::: {#locate-example .example}
 ::: {.title}
 **Example: NixOS Module for the "locate" Service**
@@ -153,6 +164,37 @@ in {
 ```
 :::
 
+::: {#exec-escaping-example .example}
+::: {.title}
+**Example: Escaping in Exec directives**
+:::
+```nix
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.echo;
+  echoAll = pkgs.writeScript "echo-all" ''
+    #! ${pkgs.runtimeShell}
+    for s in "$@"; do
+      printf '%s\n' "$s"
+    done
+  '';
+  args = [ "a%Nything" "lang=\${LANG}" ";" "/bin/sh -c date" ];
+in {
+  systemd.services.echo =
+    { description = "Echo to the journal";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "oneshot";
+      serviceConfig.ExecStart = ''
+        ${echoAll} ${utils.escapeSystemdExecArgs args}
+      '';
+    };
+}
+```
+:::
+
 ```{=docbook}
 <xi:include href="option-declarations.section.xml" />
 <xi:include href="option-types.section.xml" />
diff --git a/nixos/doc/manual/development/writing-nixos-tests.section.md b/nixos/doc/manual/development/writing-nixos-tests.section.md
index 7de57d0d2a379..da965ce09e369 100644
--- a/nixos/doc/manual/development/writing-nixos-tests.section.md
+++ b/nixos/doc/manual/development/writing-nixos-tests.section.md
@@ -5,15 +5,9 @@ A NixOS test is a Nix expression that has the following structure:
 ```nix
 import ./make-test-python.nix {
 
-  # Either the configuration of a single machine:
-  machine =
-    { config, pkgs, ... }:
-    { configuration…
-    };
-
-  # Or a set of machines:
+  # One or more machines:
   nodes =
-    { machine1 =
+    { machine =
         { config, pkgs, ... }: { … };
       machine2 =
         { config, pkgs, ... }: { … };
@@ -29,17 +23,16 @@ import ./make-test-python.nix {
 
 The attribute `testScript` is a bit of Python code that executes the
 test (described below). During the test, it will start one or more
-virtual machines, the configuration of which is described by the
-attribute `machine` (if you need only one machine in your test) or by
-the attribute `nodes` (if you need multiple machines). For instance,
-[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix)
-only needs a single machine to test whether users can log in
+virtual machines, the configuration of which is described by
+the attribute `nodes`.
+
+An example of a single-node test is
+[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix).
+It only needs a single machine to test whether users can log in
 on the virtual console, whether device ownership is correctly maintained
-when switching between consoles, and so on. On the other hand,
-[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix),
-which tests NFS client and server functionality in the
-Linux kernel (including whether locks are maintained across server
-crashes), requires three machines: a server and two clients.
+when switching between consoles, and so on. An interesting multi-node test is
+[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix).
+It uses two client nodes to test correct locking across server crashes.
 
 There are a few special NixOS configuration options for test VMs:
 
@@ -67,8 +60,7 @@ The test script is a sequence of Python statements that perform various
 actions, such as starting VMs, executing commands in the VMs, and so on.
 Each virtual machine is represented as an object stored in the variable
 `name` if this is also the identifier of the machine in the declarative
-config. If you didn\'t specify multiple machines using the `nodes`
-attribute, it is just `machine`. The following example starts the
+config. If you specified a node `nodes.machine`, the following example starts the
 machine, waits until it has finished booting, then executes a command
 and checks that the output is more-or-less correct:
 
@@ -79,7 +71,7 @@ if not "Linux" in machine.succeed("uname"):
   raise Exception("Wrong OS")
 ```
 
-The first line is actually unnecessary; machines are implicitly started
+The first line is technically unnecessary; machines are implicitly started
 when you first execute an action on them (such as `wait_for_unit` or
 `succeed`). If you have multiple machines, you can speed up the test by
 starting them in parallel:
@@ -158,37 +150,51 @@ The following methods are available on machine objects:
     e.g., `send_chars("foobar\n")` will type the string `foobar`
     followed by the Enter key.
 
+`send_console`
+
+:   Send keys to the kernel console. This allows interaction with the systemd
+    emergency mode, for example. Takes a string that is sent, e.g.,
+    `send_console("\n\nsystemctl default\n")`.
+
 `execute`
 
 :   Execute a shell command, returning a list `(status, stdout)`.
+
+    Commands are run with `set -euo pipefail` set:
+
+    -   If several commands are separated by `;` and one fails, the
+        command as a whole will fail.
+
+    -   For pipelines, the last non-zero exit status will be returned
+        (if there is one; otherwise zero will be returned).
+
+    -   Dereferencing unset variables fails the command.
+
+    -   It will wait for stdout to be closed.
+
     If the command detaches, it must close stdout, as `execute` will wait
     for this to consume all output reliably. This can be achieved by
     redirecting stdout to stderr `>&2`, to `/dev/console`, `/dev/null` or
     a file. Examples of detaching commands are `sleep 365d &`, where the
     shell forks a new process that can write to stdout and `xclip -i`, where
     the `xclip` command itself forks without closing stdout.
+
     Takes an optional parameter `check_return` that defaults to `True`.
     Setting this parameter to `False` will not check for the return code
     and return -1 instead. This can be used for commands that shut down
     the VM and would therefore break the pipe that would be used for
     retrieving the return code.
 
+    A timeout for the command can be specified (in seconds) using the optional
+    `timeout` parameter, e.g., `execute(cmd, timeout=10)` or
+    `execute(cmd, timeout=None)`. The default is 900 seconds.
+
 `succeed`
 
 :   Execute a shell command, raising an exception if the exit status is
-    not zero, otherwise returning the standard output. Commands are run
-    with `set -euo pipefail` set:
-
-    -   If several commands are separated by `;` and one fails, the
-        command as a whole will fail.
-
-    -   For pipelines, the last non-zero exit status will be returned
-        (if there is one, zero will be returned otherwise).
-
-    -   Dereferencing unset variables fail the command.
-
-    -   It will wait for stdout to be closed. See `execute` for the
-        implications.
+    not zero, otherwise returning the standard output. Similar to `execute`,
+    except that the timeout is `None` by default. See `execute` for details on
+    command execution.
 
 `fail`
 
@@ -198,10 +204,13 @@ The following methods are available on machine objects:
 `wait_until_succeeds`
 
 :   Repeat a shell command with 1-second intervals until it succeeds.
+    Has a default timeout of 900 seconds which can be modified, e.g.
+    `wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on
+    command execution.
 
 `wait_until_fails`
 
-:   Repeat a shell command with 1-second intervals until it fails.
+:   Like `wait_until_succeeds`, but repeating the command until it fails.
 
 `wait_for_unit`
 
@@ -272,6 +281,13 @@ The following methods are available on machine objects:
     Killing the interactive session with `Ctrl-d` or `Ctrl-c` also ends
     the guest session.
 
+`console_interact`
+
+:   Allows you to directly interact with QEMU's stdin. This should
+    only be used during test development, not in production tests.
+    Output from QEMU is only read line-wise. `Ctrl-c` kills QEMU and
+    `Ctrl-d` closes console and returns to the test runner.
+
 To test user units declared by `systemd.user.services` the optional
 `user` argument can be used:
 
@@ -290,7 +306,7 @@ For faster dev cycles it\'s also possible to disable the code-linters
 ```nix
 import ./make-test-python.nix {
   skipLint = true;
-  machine =
+  nodes.machine =
     { config, pkgs, ... }:
     { configuration…
     };
@@ -316,6 +332,19 @@ repository):
     '';
 ```
 
+Similarly, the type checking of test scripts can be disabled in the following
+way:
+
+```nix
+import ./make-test-python.nix {
+  skipTypeCheck = true;
+  nodes.machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+}
+```
+
 ## Failing tests early {#ssec-failing-tests-early}
 
 To fail tests early when certain invariables are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following:
@@ -333,7 +362,6 @@ with foo_running:
     ...  # Put `foo` through its paces
 ```
 
-
 `polling_condition` takes the following (optional) arguments:
 
 `seconds_interval`
@@ -364,3 +392,28 @@ with foo_running:
     def foo_running():
         machine.succeed("pgrep -x foo")
     ```
+
+## Adding Python packages to the test script {#ssec-python-packages-in-test-script}
+
+When additional Python libraries are required in the test script, they can be
+added using the parameter `extraPythonPackages`. For example, you could add
+`numpy` like this:
+
+```nix
+import ./make-test-python.nix
+{
+  extraPythonPackages = p: [ p.numpy ];
+
+  nodes = { };
+
+  # Type checking on extra packages doesn't work yet
+  skipTypeCheck = true;
+
+  testScript = ''
+    import numpy as np
+    assert str(np.zeros(4) == "array([0., 0., 0., 0.])")
+  '';
+}
+```
+
+In that case, `numpy` is chosen from the generic `python3Packages`.
diff --git a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
index 7b35520d567bf..b8179dca1f8bd 100644
--- a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
+++ b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
@@ -48,8 +48,8 @@ containers.database = {
     <literal>configuration.nix</literal> and run
     <literal>nixos-rebuild switch</literal>. Note that this will not
     delete the root directory of the container in
-    <literal>/var/lib/containers</literal>. Containers can be destroyed
-    using the imperative method:
+    <literal>/var/lib/nixos-containers</literal>. Containers can be
+    destroyed using the imperative method:
     <literal>nixos-container destroy foo</literal>.
   </para>
   <para>
diff --git a/nixos/doc/manual/from_md/administration/imperative-containers.section.xml b/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
index 59ecfdee5af08..865fc46893981 100644
--- a/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
+++ b/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
@@ -14,8 +14,9 @@
 </programlisting>
   <para>
     This creates the container’s root directory in
-    <literal>/var/lib/containers/foo</literal> and a small configuration
-    file in <literal>/etc/containers/foo.conf</literal>. It also builds
+    <literal>/var/lib/nixos-containers/foo</literal> and a small
+    configuration file in
+    <literal>/etc/nixos-containers/foo.conf</literal>. It also builds
     the container’s initial system configuration and stores it in
     <literal>/nix/var/nix/profiles/per-container/foo/system</literal>.
     You can modify the initial configuration of the container on the
diff --git a/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml b/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml
index 4fa40d61966e7..07f541666cbe1 100644
--- a/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml
+++ b/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml
@@ -2,40 +2,50 @@
   <title>Adding Custom Packages</title>
   <para>
     It’s possible that a package you need is not available in NixOS. In
-    that case, you can do two things. First, you can clone the Nixpkgs
-    repository, add the package to your clone, and (optionally) submit a
-    patch or pull request to have it accepted into the main Nixpkgs
-    repository. This is described in detail in the
-    <link xlink:href="https://nixos.org/nixpkgs/manual">Nixpkgs
-    manual</link>. In short, you clone Nixpkgs:
+    that case, you can do two things. Either you can package it with
+    Nix, or you can try to use prebuilt packages from upstream. Due to
+    the peculiarities of NixOS, it is important to note that building
+    software from source is often easier than using pre-built
+    executables.
   </para>
-  <programlisting>
+  <section xml:id="sec-custom-packages-nix">
+    <title>Building with Nix</title>
+    <para>
+      This can be done either in-tree or out-of-tree. For an in-tree
+      build, you can clone the Nixpkgs repository, add the package to
+      your clone, and (optionally) submit a patch or pull request to
+      have it accepted into the main Nixpkgs repository. This is
+      described in detail in the
+      <link xlink:href="https://nixos.org/nixpkgs/manual">Nixpkgs
+      manual</link>. In short, you clone Nixpkgs:
+    </para>
+    <programlisting>
 $ git clone https://github.com/NixOS/nixpkgs
 $ cd nixpkgs
 </programlisting>
-  <para>
-    Then you write and test the package as described in the Nixpkgs
-    manual. Finally, you add it to
-    <xref linkend="opt-environment.systemPackages" />, e.g.
-  </para>
-  <programlisting language="bash">
+    <para>
+      Then you write and test the package as described in the Nixpkgs
+      manual. Finally, you add it to
+      <xref linkend="opt-environment.systemPackages" />, e.g.
+    </para>
+    <programlisting language="bash">
 environment.systemPackages = [ pkgs.my-package ];
 </programlisting>
-  <para>
-    and you run <literal>nixos-rebuild</literal>, specifying your own
-    Nixpkgs tree:
-  </para>
-  <programlisting>
+    <para>
+      and you run <literal>nixos-rebuild</literal>, specifying your own
+      Nixpkgs tree:
+    </para>
+    <programlisting>
 # nixos-rebuild switch -I nixpkgs=/path/to/my/nixpkgs
 </programlisting>
-  <para>
-    The second possibility is to add the package outside of the Nixpkgs
-    tree. For instance, here is how you specify a build of the
-    <link xlink:href="https://www.gnu.org/software/hello/">GNU
-    Hello</link> package directly in
-    <literal>configuration.nix</literal>:
-  </para>
-  <programlisting language="bash">
+    <para>
+      The second possibility is to add the package outside of the
+      Nixpkgs tree. For instance, here is how you specify a build of the
+      <link xlink:href="https://www.gnu.org/software/hello/">GNU
+      Hello</link> package directly in
+      <literal>configuration.nix</literal>:
+    </para>
+    <programlisting language="bash">
 environment.systemPackages =
   let
     my-hello = with pkgs; stdenv.mkDerivation rec {
@@ -48,17 +58,17 @@ environment.systemPackages =
   in
   [ my-hello ];
 </programlisting>
-  <para>
-    Of course, you can also move the definition of
-    <literal>my-hello</literal> into a separate Nix expression, e.g.
-  </para>
-  <programlisting language="bash">
+    <para>
+      Of course, you can also move the definition of
+      <literal>my-hello</literal> into a separate Nix expression, e.g.
+    </para>
+    <programlisting language="bash">
 environment.systemPackages = [ (import ./my-hello.nix) ];
 </programlisting>
-  <para>
-    where <literal>my-hello.nix</literal> contains:
-  </para>
-  <programlisting language="bash">
+    <para>
+      where <literal>my-hello.nix</literal> contains:
+    </para>
+    <programlisting language="bash">
 with import &lt;nixpkgs&gt; {}; # bring all of Nixpkgs into scope
 
 stdenv.mkDerivation rec {
@@ -69,12 +79,40 @@ stdenv.mkDerivation rec {
   };
 }
 </programlisting>
-  <para>
-    This allows testing the package easily:
-  </para>
-  <programlisting>
+    <para>
+      This allows testing the package easily:
+    </para>
+    <programlisting>
 $ nix-build my-hello.nix
 $ ./result/bin/hello
 Hello, world!
 </programlisting>
+  </section>
+  <section xml:id="sec-custom-packages-prebuilt">
+    <title>Using pre-built executables</title>
+    <para>
+      Most pre-built executables will not work on NixOS. There are two
+      notable exceptions: flatpaks and AppImages. For flatpaks see the
+      <link linkend="module-services-flatpak">dedicated section</link>.
+      AppImages will not run <quote>as-is</quote> on NixOS. First you
+      need to install <literal>appimage-run</literal>: add to
+      <literal>/etc/nixos/configuration.nix</literal>
+    </para>
+    <programlisting language="bash">
+environment.systemPackages = [ pkgs.appimage-run ];
+</programlisting>
+    <para>
+      Then instead of running the AppImage <quote>as-is</quote>, run
+      <literal>appimage-run foo.appimage</literal>.
+    </para>
+    <para>
+      To make other pre-built executables work on NixOS, you need to
+      package them with Nix and special helpers like
+      <literal>autoPatchelfHook</literal> or
+      <literal>buildFHSUserEnv</literal>. See the
+      <link xlink:href="https://nixos.org/nixpkgs/manual">Nixpkgs
+      manual</link> for details. This is complex and often doing a
+      source build is easier.
+    </para>
+  </section>
 </section>
diff --git a/nixos/doc/manual/from_md/configuration/xfce.chapter.xml b/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
index f96ef2e8c483c..42e70d1d81d30 100644
--- a/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
+++ b/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
@@ -27,13 +27,19 @@ services.picom = {
     <literal>pkgs.xfce</literal>.
   </para>
   <section xml:id="sec-xfce-thunar-plugins">
-    <title>Thunar Plugins</title>
+    <title>Thunar</title>
     <para>
-      If you'd like to add extra plugins to Thunar, add them to
-      <xref linkend="opt-services.xserver.desktopManager.xfce.thunarPlugins" />.
-      You shouldn't just add them to
+      Thunar (the Xfce file manager) is automatically enabled when Xfce
+      is enabled. To enable Thunar without enabling Xfce, use the
+      configuration option <xref linkend="opt-programs.thunar.enable" />
+      instead of simply adding <literal>pkgs.xfce.thunar</literal> to
       <xref linkend="opt-environment.systemPackages" />.
     </para>
+    <para>
+      If you'd like to add extra plugins to Thunar, add them to
+      <xref linkend="opt-programs.thunar.plugins" />. You shouldn't just
+      add them to <xref linkend="opt-environment.systemPackages" />.
+    </para>
   </section>
   <section xml:id="sec-xfce-troubleshooting">
     <title>Troubleshooting</title>
diff --git a/nixos/doc/manual/from_md/development/activation-script.section.xml b/nixos/doc/manual/from_md/development/activation-script.section.xml
new file mode 100644
index 0000000000000..981ebf37e60f0
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/activation-script.section.xml
@@ -0,0 +1,150 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-activation-script">
+  <title>Activation script</title>
+  <para>
+    The activation script is a bash script called to activate the new
+    configuration which resides in a NixOS system in
+    <literal>$out/activate</literal>. Since its contents depend on your
+    system configuration, the contents may differ. This chapter explains
+    how the script works in general and some common NixOS snippets.
+    Please be aware that the script is executed on every boot and system
+    switch, so tasks that can be performed in other places should be
+    performed there (for example letting a directory of a service be
+    created by systemd using mechanisms like
+    <literal>StateDirectory</literal>,
+    <literal>CacheDirectory</literal>, … or if that’s not possible using
+    <literal>preStart</literal> of the service).
+  </para>
+  <para>
+    Activation scripts are defined as snippets using
+    <xref linkend="opt-system.activationScripts" />. They can either be
+    a simple multiline string or an attribute set that can depend on
+    other snippets. The builder for the activation script will take
+    these dependencies into account and order the snippets accordingly.
+    As a simple example:
+  </para>
+  <programlisting language="bash">
+system.activationScripts.my-activation-script = {
+  deps = [ &quot;etc&quot; ];
+  # supportsDryActivation = true;
+  text = ''
+    echo &quot;Hallo i bims&quot;
+  '';
+};
+</programlisting>
+  <para>
+    This example creates an activation script snippet that is run after
+    the <literal>etc</literal> snippet. The special variable
+    <literal>supportsDryActivation</literal> can be set so the snippet
+    is also run when <literal>nixos-rebuild dry-activate</literal> is
+    run. To differentiate between real and dry activation, the
+    <literal>$NIXOS_ACTION</literal> environment variable can be read
+    which is set to <literal>dry-activate</literal> when a dry
+    activation is done.
+  </para>
+  <para>
+    An activation script can write to special files instructing
+    <literal>switch-to-configuration</literal> to restart/reload units.
+    The script will take these requests into account and will
+    incorperate the unit configuration as described above. This means
+    that the activation script will <quote>fake</quote> a modified unit
+    file and <literal>switch-to-configuration</literal> will act
+    accordingly. By doing so, configuration like
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>
+    is respected. Since the activation script is run
+    <emphasis role="strong">after</emphasis> services are already
+    stopped,
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>
+    cannot be taken into account anymore and the unit is always
+    restarted instead of being stopped and started afterwards.
+  </para>
+  <para>
+    The files that can be written to are
+    <literal>/run/nixos/activation-restart-list</literal> and
+    <literal>/run/nixos/activation-reload-list</literal> with their
+    respective counterparts for dry activation being
+    <literal>/run/nixos/dry-activation-restart-list</literal> and
+    <literal>/run/nixos/dry-activation-reload-list</literal>. Those
+    files can contain newline-separated lists of unit names where
+    duplicates are being ignored. These files are not create
+    automatically and activation scripts must take the possiblility into
+    account that they have to create them first.
+  </para>
+  <section xml:id="sec-activation-script-nixos-snippets">
+    <title>NixOS snippets</title>
+    <para>
+      There are some snippets NixOS enables by default because disabling
+      them would most likely break your system. This section lists a few
+      of them and what they do:
+    </para>
+    <itemizedlist spacing="compact">
+      <listitem>
+        <para>
+          <literal>binsh</literal> creates <literal>/bin/sh</literal>
+          which points to the runtime shell
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>etc</literal> sets up the contents of
+          <literal>/etc</literal>, this includes systemd units and
+          excludes <literal>/etc/passwd</literal>,
+          <literal>/etc/group</literal>, and
+          <literal>/etc/shadow</literal> (which are managed by the
+          <literal>users</literal> snippet)
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>hostname</literal> sets the system’s hostname in the
+          kernel (not in <literal>/etc</literal>)
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>modprobe</literal> sets the path to the
+          <literal>modprobe</literal> binary for module auto-loading
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>nix</literal> prepares the nix store and adds a
+          default initial channel
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>specialfs</literal> is responsible for mounting
+          filesystems like <literal>/proc</literal> and
+          <literal>sys</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>users</literal> creates and removes users and groups
+          by managing <literal>/etc/passwd</literal>,
+          <literal>/etc/group</literal> and
+          <literal>/etc/shadow</literal>. This also creates home
+          directories
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>usrbinenv</literal> creates
+          <literal>/usr/bin/env</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>var</literal> creates some directories in
+          <literal>/var</literal> that are not service-specific
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>wrappers</literal> creates setuid wrappers like
+          <literal>ping</literal> and <literal>sudo</literal>
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+</section>
diff --git a/nixos/doc/manual/from_md/development/option-declarations.section.xml b/nixos/doc/manual/from_md/development/option-declarations.section.xml
index 0eeffae628e1a..03ec48f35fd78 100644
--- a/nixos/doc/manual/from_md/development/option-declarations.section.xml
+++ b/nixos/doc/manual/from_md/development/option-declarations.section.xml
@@ -38,9 +38,11 @@ options = {
       <listitem>
         <para>
           The type of the option (see
-          <xref linkend="sec-option-types" />). It may be omitted, but
-          that’s not advisable since it may lead to errors that are hard
-          to diagnose.
+          <xref linkend="sec-option-types" />). This argument is
+          mandatory for nixpkgs modules. Setting this is highly
+          recommended for the sake of documentation and type checking.
+          In case it is not set, a fallback type with unspecified
+          behavior is used.
         </para>
       </listitem>
     </varlistentry>
@@ -92,7 +94,17 @@ options = {
       <listitem>
         <para>
           A textual description of the option, in DocBook format, that
-          will be included in the NixOS manual.
+          will be included in the NixOS manual. During the migration
+          process from DocBook to CommonMark the description may also be
+          written in CommonMark, but has to be wrapped in
+          <literal>lib.mdDoc</literal> to differentiate it from DocBook.
+          See the nixpkgs manual for
+          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-contributing-markup">the
+          list of CommonMark extensions</link> supported by NixOS
+          documentation.
+        </para>
+        <para>
+          New documentation should preferably be written as CommonMark.
         </para>
       </listitem>
     </varlistentry>
@@ -181,14 +193,14 @@ lib.mkOption {
         <programlisting language="bash">
 lib.mkPackageOption pkgs &quot;GHC&quot; {
   default = [ &quot;ghc&quot; ];
-  example = &quot;pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
+  example = &quot;pkgs.haskell.package.ghc923.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
 }
 # is like
 lib.mkOption {
   type = lib.types.package;
   default = pkgs.ghc;
   defaultText = lib.literalExpression &quot;pkgs.ghc&quot;;
-  example = lib.literalExpression &quot;pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
+  example = lib.literalExpression &quot;pkgs.haskell.package.ghc923.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
   description = &quot;The GHC package to use.&quot;;
 }
 </programlisting>
@@ -215,21 +227,22 @@ lib.mkOption {
             manager backend (sddm, gdm ...).
           </para>
           <para>
-            There are two approach to this module structure:
+            There are two approaches we could take with this module
+            structure:
           </para>
           <itemizedlist>
             <listitem>
               <para>
-                Managing the display managers independently by adding an
-                enable option to every display manager module backend.
-                (NixOS)
+                Configuring the display managers independently by adding
+                an enable option to every display manager module
+                backend. (NixOS)
               </para>
             </listitem>
             <listitem>
               <para>
-                Managing the display managers in the central module by
-                adding an option to select which display manager backend
-                to use.
+                Configuring the display managers in the central module
+                by adding an option to select which display manager
+                backend to use.
               </para>
             </listitem>
           </itemizedlist>
@@ -238,16 +251,16 @@ lib.mkOption {
           </para>
           <para>
             Making backends independent can quickly become hard to
-            manage. For display managers, there can be only one enabled
-            at a time, but the type system can not enforce this
-            restriction as there is no relation between each backend
+            manage. For display managers, there can only be one enabled
+            at a time, but the type system cannot enforce this
+            restriction as there is no relation between each backend’s
             <literal>enable</literal> option. As a result, this
-            restriction has to be done explicitely by adding assertions
+            restriction has to be done explicitly by adding assertions
             in each display manager backend module.
           </para>
           <para>
-            On the other hand, managing the display managers backends in
-            the central module will require to change the central module
+            On the other hand, managing the display manager backends in
+            the central module will require changing the central module
             option every time a new backend is added or removed.
           </para>
           <para>
@@ -268,7 +281,7 @@ lib.mkOption {
           <para>
             As a result, <literal>displayManager.enable</literal> option
             values can be added without changing the main service module
-            file and the type system automatically enforce that there
+            file and the type system automatically enforces that there
             can only be a single display manager enabled.
           </para>
           <anchor xml:id="ex-option-declaration-eot-service" />
diff --git a/nixos/doc/manual/from_md/development/option-types.section.xml b/nixos/doc/manual/from_md/development/option-types.section.xml
index 76ffb6f837c35..929d5302ed417 100644
--- a/nixos/doc/manual/from_md/development/option-types.section.xml
+++ b/nixos/doc/manual/from_md/development/option-types.section.xml
@@ -30,10 +30,10 @@
         </term>
         <listitem>
           <para>
-            A filesystem path, defined as anything that when coerced to
-            a string starts with a slash. Even if derivations can be
-            considered as path, the more specific
-            <literal>types.package</literal> should be preferred.
+            A filesystem path is anything that starts with a slash when
+            coerced to a string. Even if derivations can be considered
+            as paths, the more specific <literal>types.package</literal>
+            should be preferred.
           </para>
         </listitem>
       </varlistentry>
@@ -43,7 +43,9 @@
         </term>
         <listitem>
           <para>
-            A derivation or a store path.
+            A top-level store path. This can be an attribute set
+            pointing to a store path, like a derivation or a flake
+            input.
           </para>
         </listitem>
       </varlistentry>
@@ -94,6 +96,39 @@
       </varlistentry>
       <varlistentry>
         <term>
+          <literal>types.raw</literal>
+        </term>
+        <listitem>
+          <para>
+            A type which doesn’t do any checking, merging or nested
+            evaluation. It accepts a single arbitrary value that is not
+            recursed into, making it useful for values coming from
+            outside the module system, such as package sets or arbitrary
+            data. Options of this type are still evaluated according to
+            priorities and conditionals, so <literal>mkForce</literal>,
+            <literal>mkIf</literal> and co. still work on the option
+            value itself, but not for any value nested within it. This
+            type should only be used when checking, merging and nested
+            evaluation are not desirable.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>types.optionType</literal>
+        </term>
+        <listitem>
+          <para>
+            The type of an option’s type. Its merging operation ensures
+            that nested options have the correct file location
+            annotated, and that if possible, multiple option definitions
+            are correctly merged together. The main use case is as the
+            type of the <literal>_module.freeformType</literal> option.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
           <literal>types.attrs</literal>
         </term>
         <listitem>
@@ -392,6 +427,43 @@
           </itemizedlist>
         </listitem>
       </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>types.deferredModule</literal>
+        </term>
+        <listitem>
+          <para>
+            Whereas <literal>submodule</literal> represents an option
+            tree, <literal>deferredModule</literal> represents a module
+            value, such as a module file or a configuration.
+          </para>
+          <para>
+            It can be set multiple times.
+          </para>
+          <para>
+            Module authors can use its value in
+            <literal>imports</literal>, in
+            <literal>submoduleWith</literal><quote>s
+            <literal>modules</literal> or in
+            <literal>evalModules</literal></quote>
+            <literal>modules</literal> parameter, among other places.
+          </para>
+          <para>
+            Note that <literal>imports</literal> must be evaluated
+            before the module fixpoint. Because of this, deferred
+            modules can only be imported into <quote>other</quote>
+            fixpoints, such as submodules.
+          </para>
+          <para>
+            One use case for this type is the type of a
+            <quote>default</quote> module that allow the user to affect
+            all submodules in an <literal>attrsOf submodule</literal> at
+            once. This is more convenient and discoverable than
+            expecting the module user to type-merge with the
+            <literal>attrsOf submodule</literal> option.
+          </para>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </section>
   <section xml:id="sec-option-types-composed">
@@ -582,6 +654,12 @@
       (<link linkend="ex-submodule-reference">Example: Submodule defined
       as a reference</link>).
     </para>
+    <para>
+      Note that even if your submodule’s options all have a default
+      value, you will still need to provide a default value (e.g. an
+      empty attribute set) if you want to allow users to leave it
+      undefined.
+    </para>
     <anchor xml:id="ex-submodule-direct" />
     <para>
       <emphasis role="strong">Example: Directly defined
diff --git a/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml b/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
index 7159b95b22b01..da2e5076c956d 100644
--- a/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
+++ b/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
@@ -26,9 +26,9 @@ machine: QEMU running (pid 8841)
     perform a build that starts a QEMU/KVM virtual machine containing a
     NixOS system. The virtual machine mounts the Nix store of the host;
     this makes VM creation very fast, as no disk image needs to be
-    created. Afterwards, you can view a pretty-printed log of the test:
+    created. Afterwards, you can view a log of the test:
   </para>
   <programlisting>
-$ firefox result/log.html
+$ nix-store --read-log result
 </programlisting>
 </section>
diff --git a/nixos/doc/manual/from_md/development/settings-options.section.xml b/nixos/doc/manual/from_md/development/settings-options.section.xml
index c9430b77579c2..d26dd96243dbe 100644
--- a/nixos/doc/manual/from_md/development/settings-options.section.xml
+++ b/nixos/doc/manual/from_md/development/settings-options.section.xml
@@ -55,6 +55,38 @@
     <variablelist>
       <varlistentry>
         <term>
+          <literal>pkgs.formats.javaProperties</literal> {
+          <emphasis><literal>comment</literal></emphasis> ?
+          <literal>&quot;Generated with Nix&quot;</literal> }
+        </term>
+        <listitem>
+          <para>
+            A function taking an attribute set with values
+          </para>
+          <variablelist>
+            <varlistentry>
+              <term>
+                <literal>comment</literal>
+              </term>
+              <listitem>
+                <para>
+                  A string to put at the start of the file in a comment.
+                  It can have multiple lines.
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+          <para>
+            It returns the <literal>type</literal>:
+            <literal>attrsOf str</literal> and a function
+            <literal>generate</literal> to build a Java
+            <literal>.properties</literal> file, taking care of the
+            correct escaping, etc.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
           <literal>pkgs.formats.json</literal> { }
         </term>
         <listitem>
@@ -137,6 +169,97 @@
           </para>
         </listitem>
       </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>pkgs.formats.elixirConf { elixir ? pkgs.elixir }</literal>
+        </term>
+        <listitem>
+          <para>
+            A function taking an attribute set with values
+          </para>
+          <variablelist>
+            <varlistentry>
+              <term>
+                <literal>elixir</literal>
+              </term>
+              <listitem>
+                <para>
+                  The Elixir package which will be used to format the
+                  generated output
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+          <para>
+            It returns a set with Elixir-Config-specific attributes
+            <literal>type</literal>, <literal>lib</literal>, and
+            <literal>generate</literal> as specified
+            <link linkend="pkgs-formats-result">below</link>.
+          </para>
+          <para>
+            The <literal>lib</literal> attribute contains functions to
+            be used in settings, for generating special Elixir values:
+          </para>
+          <variablelist>
+            <varlistentry>
+              <term>
+                <literal>mkRaw elixirCode</literal>
+              </term>
+              <listitem>
+                <para>
+                  Outputs the given string as raw Elixir code
+                </para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>
+                <literal>mkGetEnv { envVariable, fallback ? null }</literal>
+              </term>
+              <listitem>
+                <para>
+                  Makes the configuration fetch an environment variable
+                  at runtime
+                </para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>
+                <literal>mkAtom atom</literal>
+              </term>
+              <listitem>
+                <para>
+                  Outputs the given string as an Elixir atom, instead of
+                  the default Elixir binary string. Note: lowercase
+                  atoms still needs to be prefixed with
+                  <literal>:</literal>
+                </para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>
+                <literal>mkTuple array</literal>
+              </term>
+              <listitem>
+                <para>
+                  Outputs the given array as an Elixir tuple, instead of
+                  the default Elixir list
+                </para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>
+                <literal>mkMap attrset</literal>
+              </term>
+              <listitem>
+                <para>
+                  Outputs the given attribute set as an Elixir map,
+                  instead of the default Elixir keyword list
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        </listitem>
+      </varlistentry>
     </variablelist>
     <para xml:id="pkgs-formats-result">
       These functions all return an attribute set with these values:
@@ -154,6 +277,19 @@
       </varlistentry>
       <varlistentry>
         <term>
+          <literal>lib</literal>
+        </term>
+        <listitem>
+          <para>
+            Utility functions for convenience, or special interactions
+            with the format. This attribute is optional. It may contain
+            inside a <literal>types</literal> attribute containing types
+            specific to this format.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
           <literal>generate</literal>
           <emphasis><literal>filename jsonValue</literal></emphasis>
         </term>
diff --git a/nixos/doc/manual/from_md/development/unit-handling.section.xml b/nixos/doc/manual/from_md/development/unit-handling.section.xml
new file mode 100644
index 0000000000000..4c980e1213a83
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/unit-handling.section.xml
@@ -0,0 +1,131 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-unit-handling">
+  <title>Unit handling</title>
+  <para>
+    To figure out what units need to be
+    started/stopped/restarted/reloaded, the script first checks the
+    current state of the system, similar to what
+    <literal>systemctl list-units</literal> shows. For each of the
+    units, the script goes through the following checks:
+  </para>
+  <itemizedlist>
+    <listitem>
+      <para>
+        Is the unit file still in the new system? If not,
+        <emphasis role="strong">stop</emphasis> the service unless it
+        sets <literal>X-StopOnRemoval</literal> in the
+        <literal>[Unit]</literal> section to <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Is it a <literal>.target</literal> unit? If so,
+        <emphasis role="strong">start</emphasis> it unless it sets
+        <literal>RefuseManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal> or
+        <literal>X-OnlyManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal>.
+        Also <emphasis role="strong">stop</emphasis> the unit again
+        unless it sets <literal>X-StopOnReconfiguration</literal> to
+        <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Are the contents of the unit files different? They are compared
+        by parsing them and comparing their contents. If they are
+        different but only <literal>X-Reload-Triggers</literal> in the
+        <literal>[Unit]</literal> section is changed,
+        <emphasis role="strong">reload</emphasis> the unit. The NixOS
+        module system allows setting these triggers with the option
+        <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadTriggers</link>.
+        There are some additional keys in the <literal>[Unit]</literal>
+        section that are ignored as well. If the unit files differ in
+        any way, the following actions are performed:
+      </para>
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>.path</literal> and <literal>.slice</literal> units
+            are ignored. There is no need to restart them since changes
+            in their values are applied by systemd when systemd is
+            reloaded.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.mount</literal> units are
+            <emphasis role="strong">reload</emphasis>ed. These mostly
+            come from the <literal>/etc/fstab</literal> parser.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.socket</literal> units are currently ignored. This
+            is to be fixed at a later point.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The rest of the units (mostly <literal>.service</literal>
+            units) are then <emphasis role="strong">reload</emphasis>ed
+            if <literal>X-ReloadIfChanged</literal> in the
+            <literal>[Service]</literal> section is set to
+            <literal>true</literal> (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadIfChanged</link>).
+            A little exception is done for units that were deactivated
+            in the meantime, for example because they require a unit
+            that got stopped before. These are
+            <emphasis role="strong">start</emphasis>ed instead of
+            reloaded.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            If the reload flag is not set, some more flags decide if the
+            unit is skipped. These flags are
+            <literal>X-RestartIfChanged</literal> in the
+            <literal>[Service]</literal> section (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>),
+            <literal>RefuseManualStop</literal> in the
+            <literal>[Unit]</literal> section, and
+            <literal>X-OnlyManualStart</literal> in the
+            <literal>[Unit]</literal> section.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Further behavior depends on the unit having
+            <literal>X-StopIfChanged</literal> in the
+            <literal>[Service]</literal> section set to
+            <literal>true</literal> (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>).
+            This is set to <literal>true</literal> by default and must
+            be explicitly turned off if not wanted. If the flag is
+            enabled, the unit is
+            <emphasis role="strong">stop</emphasis>ped and then
+            <emphasis role="strong">start</emphasis>ed. If not, the unit
+            is <emphasis role="strong">restart</emphasis>ed. The goal of
+            the flag is to make sure that the new unit never runs in the
+            old environment which is still in place before the
+            activation script is run. This behavior is different when
+            the service is socket-activated, as outlined in the
+            following steps.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The last thing that is taken into account is whether the
+            unit is a service and socket-activated. If
+            <literal>X-StopIfChanged</literal> is
+            <emphasis role="strong">not</emphasis> set, the service is
+            <emphasis role="strong">restart</emphasis>ed with the
+            others. If it is set, both the service and the socket are
+            <emphasis role="strong">stop</emphasis>ped and the socket is
+            <emphasis role="strong">start</emphasis>ed, leaving socket
+            activation to start the service when it’s needed.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </listitem>
+  </itemizedlist>
+</section>
diff --git a/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
new file mode 100644
index 0000000000000..66ba792ddacb4
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
@@ -0,0 +1,122 @@
+<chapter xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-switching-systems">
+  <title>What happens during a system switch?</title>
+  <para>
+    Running <literal>nixos-rebuild switch</literal> is one of the more
+    common tasks under NixOS. This chapter explains some of the
+    internals of this command to make it simpler for new module
+    developers to configure their units correctly and to make it easier
+    to understand what is happening and why for curious administrators.
+  </para>
+  <para>
+    <literal>nixos-rebuild</literal>, like many deployment solutions,
+    calls <literal>switch-to-configuration</literal> which resides in a
+    NixOS system at <literal>$out/bin/switch-to-configuration</literal>.
+    The script is called with the action that is to be performed like
+    <literal>switch</literal>, <literal>test</literal>,
+    <literal>boot</literal>. There is also the
+    <literal>dry-activate</literal> action which does not really perform
+    the actions but rather prints what it would do if you called it with
+    <literal>test</literal>. This feature can be used to check what
+    service states would be changed if the configuration was switched
+    to.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>boot</literal>, the bootloader is updated first so the
+    configuration will be the next one to boot. Unless
+    <literal>NIXOS_NO_SYNC</literal> is set to <literal>1</literal>,
+    <literal>/nix/store</literal> is synced to disk.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>test</literal>, the currently running system is inspected
+    and the actions to switch to the new system are calculated. This
+    process takes two data sources into account:
+    <literal>/etc/fstab</literal> and the current systemd status. Mounts
+    and swaps are read from <literal>/etc/fstab</literal> and the
+    corresponding actions are generated. If a new mount is added, for
+    example, the proper <literal>.mount</literal> unit is marked to be
+    started. The current systemd state is inspected, the difference
+    between the current system and the desired configuration is
+    calculated and actions are generated to get to this state. There are
+    a lot of nuances that can be controlled by the units which are
+    explained here.
+  </para>
+  <para>
+    After calculating what should be done, the actions are carried out.
+    The order of actions is always the same:
+  </para>
+  <itemizedlist spacing="compact">
+    <listitem>
+      <para>
+        Stop units (<literal>systemctl stop</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Run activation script (<literal>$out/activate</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        See if the activation script requested more units to restart
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart systemd if needed
+        (<literal>systemd daemon-reexec</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Forget about the failed state of units
+        (<literal>systemctl reset-failed</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd (<literal>systemctl daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd user instances
+        (<literal>systemctl --user daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Set up tmpfiles (<literal>systemd-tmpfiles --create</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload units (<literal>systemctl reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart units (<literal>systemctl restart</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Start units (<literal>systemctl start</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Inspect what changed during these actions and print units that
+        failed and that were newly started
+      </para>
+    </listitem>
+  </itemizedlist>
+  <para>
+    Most of these actions are either self-explaining but some of them
+    have to do with our units or the activation script. For this reason,
+    these topics are explained in the next sections.
+  </para>
+  <xi:include href="unit-handling.section.xml" />
+  <xi:include href="activation-script.section.xml" />
+</chapter>
diff --git a/nixos/doc/manual/from_md/development/writing-modules.chapter.xml b/nixos/doc/manual/from_md/development/writing-modules.chapter.xml
index e33c24f4f12c2..367731eda0900 100644
--- a/nixos/doc/manual/from_md/development/writing-modules.chapter.xml
+++ b/nixos/doc/manual/from_md/development/writing-modules.chapter.xml
@@ -122,6 +122,25 @@
     services) and <literal>systemd.timers</literal> (the list of
     commands to be executed periodically by <literal>systemd</literal>).
   </para>
+  <para>
+    Care must be taken when writing systemd services using
+    <literal>Exec*</literal> directives. By default systemd performs
+    substitution on <literal>%&lt;char&gt;</literal> specifiers in these
+    directives, expands environment variables from
+    <literal>$FOO</literal> and <literal>${FOO}</literal>, splits
+    arguments on whitespace, and splits commands on
+    <literal>;</literal>. All of these must be escaped to avoid
+    unexpected substitution or splitting when interpolating into an
+    <literal>Exec*</literal> directive, e.g. when using an
+    <literal>extraArgs</literal> option to pass additional arguments to
+    the service. The functions
+    <literal>utils.escapeSystemdExecArg</literal> and
+    <literal>utils.escapeSystemdExecArgs</literal> are provided for
+    this, see <link linkend="exec-escaping-example">Example: Escaping in
+    Exec directives</link> for an example. When using these functions
+    system environment substitution should <emphasis>not</emphasis> be
+    disabled explicitly.
+  </para>
   <anchor xml:id="locate-example" />
   <para>
     <emphasis role="strong">Example: NixOS Module for the
@@ -184,6 +203,36 @@ in {
   };
 }
 </programlisting>
+  <anchor xml:id="exec-escaping-example" />
+  <para>
+    <emphasis role="strong">Example: Escaping in Exec
+    directives</emphasis>
+  </para>
+  <programlisting language="bash">
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.echo;
+  echoAll = pkgs.writeScript &quot;echo-all&quot; ''
+    #! ${pkgs.runtimeShell}
+    for s in &quot;$@&quot;; do
+      printf '%s\n' &quot;$s&quot;
+    done
+  '';
+  args = [ &quot;a%Nything&quot; &quot;lang=\${LANG}&quot; &quot;;&quot; &quot;/bin/sh -c date&quot; ];
+in {
+  systemd.services.echo =
+    { description = &quot;Echo to the journal&quot;;
+      wantedBy = [ &quot;multi-user.target&quot; ];
+      serviceConfig.Type = &quot;oneshot&quot;;
+      serviceConfig.ExecStart = ''
+        ${echoAll} ${utils.escapeSystemdExecArgs args}
+      '';
+    };
+}
+</programlisting>
   <xi:include href="option-declarations.section.xml" />
   <xi:include href="option-types.section.xml" />
   <xi:include href="option-def.section.xml" />
diff --git a/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml b/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
index 45c9c40c6095c..7e4af03829067 100644
--- a/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
+++ b/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
@@ -6,15 +6,9 @@
   <programlisting language="bash">
 import ./make-test-python.nix {
 
-  # Either the configuration of a single machine:
-  machine =
-    { config, pkgs, ... }:
-    { configuration…
-    };
-
-  # Or a set of machines:
+  # One or more machines:
   nodes =
-    { machine1 =
+    { machine =
         { config, pkgs, ... }: { … };
       machine2 =
         { config, pkgs, ... }: { … };
@@ -31,18 +25,18 @@ import ./make-test-python.nix {
     The attribute <literal>testScript</literal> is a bit of Python code
     that executes the test (described below). During the test, it will
     start one or more virtual machines, the configuration of which is
-    described by the attribute <literal>machine</literal> (if you need
-    only one machine in your test) or by the attribute
-    <literal>nodes</literal> (if you need multiple machines). For
-    instance,
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>
-    only needs a single machine to test whether users can log in on the
-    virtual console, whether device ownership is correctly maintained
-    when switching between consoles, and so on. On the other hand,
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix"><literal>nfs/simple.nix</literal></link>,
-    which tests NFS client and server functionality in the Linux kernel
-    (including whether locks are maintained across server crashes),
-    requires three machines: a server and two clients.
+    described by the attribute <literal>nodes</literal>.
+  </para>
+  <para>
+    An example of a single-node test is
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>.
+    It only needs a single machine to test whether users can log in on
+    the virtual console, whether device ownership is correctly
+    maintained when switching between consoles, and so on. An
+    interesting multi-node test is
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix"><literal>nfs/simple.nix</literal></link>.
+    It uses two client nodes to test correct locking across server
+    crashes.
   </para>
   <para>
     There are a few special NixOS configuration options for test VMs:
@@ -94,9 +88,8 @@ import ./make-test-python.nix {
     various actions, such as starting VMs, executing commands in the
     VMs, and so on. Each virtual machine is represented as an object
     stored in the variable <literal>name</literal> if this is also the
-    identifier of the machine in the declarative config. If you didn't
-    specify multiple machines using the <literal>nodes</literal>
-    attribute, it is just <literal>machine</literal>. The following
+    identifier of the machine in the declarative config. If you
+    specified a node <literal>nodes.machine</literal>, the following
     example starts the machine, waits until it has finished booting,
     then executes a command and checks that the output is more-or-less
     correct:
@@ -108,7 +101,7 @@ if not &quot;Linux&quot; in machine.succeed(&quot;uname&quot;):
   raise Exception(&quot;Wrong OS&quot;)
 </programlisting>
   <para>
-    The first line is actually unnecessary; machines are implicitly
+    The first line is technically unnecessary; machines are implicitly
     started when you first execute an action on them (such as
     <literal>wait_for_unit</literal> or <literal>succeed</literal>). If
     you have multiple machines, you can speed up the test by starting
@@ -263,40 +256,27 @@ start_all()
       </varlistentry>
       <varlistentry>
         <term>
-          <literal>execute</literal>
+          <literal>send_console</literal>
         </term>
         <listitem>
           <para>
-            Execute a shell command, returning a list
-            <literal>(status, stdout)</literal>. If the command
-            detaches, it must close stdout, as
-            <literal>execute</literal> will wait for this to consume all
-            output reliably. This can be achieved by redirecting stdout
-            to stderr <literal>&gt;&amp;2</literal>, to
-            <literal>/dev/console</literal>,
-            <literal>/dev/null</literal> or a file. Examples of
-            detaching commands are <literal>sleep 365d &amp;</literal>,
-            where the shell forks a new process that can write to stdout
-            and <literal>xclip -i</literal>, where the
-            <literal>xclip</literal> command itself forks without
-            closing stdout. Takes an optional parameter
-            <literal>check_return</literal> that defaults to
-            <literal>True</literal>. Setting this parameter to
-            <literal>False</literal> will not check for the return code
-            and return -1 instead. This can be used for commands that
-            shut down the VM and would therefore break the pipe that
-            would be used for retrieving the return code.
+            Send keys to the kernel console. This allows interaction
+            with the systemd emergency mode, for example. Takes a string
+            that is sent, e.g.,
+            <literal>send_console(&quot;\n\nsystemctl default\n&quot;)</literal>.
           </para>
         </listitem>
       </varlistentry>
       <varlistentry>
         <term>
-          <literal>succeed</literal>
+          <literal>execute</literal>
         </term>
         <listitem>
           <para>
-            Execute a shell command, raising an exception if the exit
-            status is not zero, otherwise returning the standard output.
+            Execute a shell command, returning a list
+            <literal>(status, stdout)</literal>.
+          </para>
+          <para>
             Commands are run with <literal>set -euo pipefail</literal>
             set:
           </para>
@@ -311,22 +291,63 @@ start_all()
             <listitem>
               <para>
                 For pipelines, the last non-zero exit status will be
-                returned (if there is one, zero will be returned
-                otherwise).
+                returned (if there is one; otherwise zero will be
+                returned).
               </para>
             </listitem>
             <listitem>
               <para>
-                Dereferencing unset variables fail the command.
+                Dereferencing unset variables fails the command.
               </para>
             </listitem>
             <listitem>
               <para>
-                It will wait for stdout to be closed. See
-                <literal>execute</literal> for the implications.
+                It will wait for stdout to be closed.
               </para>
             </listitem>
           </itemizedlist>
+          <para>
+            If the command detaches, it must close stdout, as
+            <literal>execute</literal> will wait for this to consume all
+            output reliably. This can be achieved by redirecting stdout
+            to stderr <literal>&gt;&amp;2</literal>, to
+            <literal>/dev/console</literal>,
+            <literal>/dev/null</literal> or a file. Examples of
+            detaching commands are <literal>sleep 365d &amp;</literal>,
+            where the shell forks a new process that can write to stdout
+            and <literal>xclip -i</literal>, where the
+            <literal>xclip</literal> command itself forks without
+            closing stdout.
+          </para>
+          <para>
+            Takes an optional parameter <literal>check_return</literal>
+            that defaults to <literal>True</literal>. Setting this
+            parameter to <literal>False</literal> will not check for the
+            return code and return -1 instead. This can be used for
+            commands that shut down the VM and would therefore break the
+            pipe that would be used for retrieving the return code.
+          </para>
+          <para>
+            A timeout for the command can be specified (in seconds)
+            using the optional <literal>timeout</literal> parameter,
+            e.g., <literal>execute(cmd, timeout=10)</literal> or
+            <literal>execute(cmd, timeout=None)</literal>. The default
+            is 900 seconds.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>succeed</literal>
+        </term>
+        <listitem>
+          <para>
+            Execute a shell command, raising an exception if the exit
+            status is not zero, otherwise returning the standard output.
+            Similar to <literal>execute</literal>, except that the
+            timeout is <literal>None</literal> by default. See
+            <literal>execute</literal> for details on command execution.
+          </para>
         </listitem>
       </varlistentry>
       <varlistentry>
@@ -347,7 +368,10 @@ start_all()
         <listitem>
           <para>
             Repeat a shell command with 1-second intervals until it
-            succeeds.
+            succeeds. Has a default timeout of 900 seconds which can be
+            modified, e.g.
+            <literal>wait_until_succeeds(cmd, timeout=10)</literal>. See
+            <literal>execute</literal> for details on command execution.
           </para>
         </listitem>
       </varlistentry>
@@ -357,8 +381,8 @@ start_all()
         </term>
         <listitem>
           <para>
-            Repeat a shell command with 1-second intervals until it
-            fails.
+            Like <literal>wait_until_succeeds</literal>, but repeating
+            the command until it fails.
           </para>
         </listitem>
       </varlistentry>
@@ -502,6 +526,21 @@ machine.systemctl(&quot;list-jobs --no-pager&quot;, &quot;any-user&quot;) # spaw
           </para>
         </listitem>
       </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>console_interact</literal>
+        </term>
+        <listitem>
+          <para>
+            Allows you to directly interact with QEMU’s stdin. This
+            should only be used during test development, not in
+            production tests. Output from QEMU is only read line-wise.
+            <literal>Ctrl-c</literal> kills QEMU and
+            <literal>Ctrl-d</literal> closes console and returns to the
+            test runner.
+          </para>
+        </listitem>
+      </varlistentry>
     </variablelist>
     <para>
       To test user units declared by
@@ -526,7 +565,7 @@ machine.wait_for_unit(&quot;xautolock.service&quot;, &quot;x-session-user&quot;)
     <programlisting language="bash">
 import ./make-test-python.nix {
   skipLint = true;
-  machine =
+  nodes.machine =
     { config, pkgs, ... }:
     { configuration…
     };
@@ -551,6 +590,19 @@ import ./make-test-python.nix {
       # fmt: on
     '';
 </programlisting>
+    <para>
+      Similarly, the type checking of test scripts can be disabled in
+      the following way:
+    </para>
+    <programlisting language="bash">
+import ./make-test-python.nix {
+  skipTypeCheck = true;
+  nodes.machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+}
+</programlisting>
   </section>
   <section xml:id="ssec-failing-tests-early">
     <title>Failing tests early</title>
@@ -613,4 +665,33 @@ def foo_running():
 ```
 </programlisting>
   </section>
+  <section xml:id="ssec-python-packages-in-test-script">
+    <title>Adding Python packages to the test script</title>
+    <para>
+      When additional Python libraries are required in the test script,
+      they can be added using the parameter
+      <literal>extraPythonPackages</literal>. For example, you could add
+      <literal>numpy</literal> like this:
+    </para>
+    <programlisting language="bash">
+import ./make-test-python.nix
+{
+  extraPythonPackages = p: [ p.numpy ];
+
+  nodes = { };
+
+  # Type checking on extra packages doesn't work yet
+  skipTypeCheck = true;
+
+  testScript = ''
+    import numpy as np
+    assert str(np.zeros(4) == &quot;array([0., 0., 0., 0.])&quot;)
+  '';
+}
+</programlisting>
+    <para>
+      In that case, <literal>numpy</literal> is chosen from the generic
+      <literal>python3Packages</literal>.
+    </para>
+  </section>
 </section>
diff --git a/nixos/doc/manual/from_md/development/building-nixos.chapter.xml b/nixos/doc/manual/from_md/installation/building-nixos.chapter.xml
index ad9349da0686d..ea2d01bebcc2f 100644
--- a/nixos/doc/manual/from_md/development/building-nixos.chapter.xml
+++ b/nixos/doc/manual/from_md/installation/building-nixos.chapter.xml
@@ -33,9 +33,14 @@
   </para>
   <section xml:id="sec-building-image-instructions">
     <title>Practical Instructions</title>
+    <para>
+      To build an ISO image for the channel
+      <literal>nixos-unstable</literal>:
+    </para>
     <programlisting>
 $ git clone https://github.com/NixOS/nixpkgs.git
 $ cd nixpkgs/nixos
+$ git switch nixos-unstable
 $ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-minimal.nix default.nix
 </programlisting>
     <para>
@@ -45,6 +50,40 @@ $ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd
 # mount -o loop -t iso9660 ./result/iso/cd.iso /mnt/iso
 </programlisting>
   </section>
+  <section xml:id="sec-building-image-drivers">
+    <title>Additional drivers or firmware</title>
+    <para>
+      If you need additional (non-distributable) drivers or firmware in
+      the installer, you might want to extend these configurations.
+    </para>
+    <para>
+      For example, to build the GNOME graphical installer ISO, but with
+      support for certain WiFi adapters present in some MacBooks, you
+      can create the following file at
+      <literal>modules/installer/cd-dvd/installation-cd-graphical-gnome-macbook.nix</literal>:
+    </para>
+    <programlisting language="bash">
+{ config, ... }:
+
+{
+  imports = [ ./installation-cd-graphical-gnome.nix ];
+
+  boot.initrd.kernelModules = [ &quot;wl&quot; ];
+
+  boot.kernelModules = [ &quot;kvm-intel&quot; &quot;wl&quot; ];
+  boot.extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];
+}
+</programlisting>
+    <para>
+      Then build it like in the example above:
+    </para>
+    <programlisting>
+$ git clone https://github.com/NixOS/nixpkgs.git
+$ cd nixpkgs/nixos
+$ export NIXPKGS_ALLOW_UNFREE=1
+$ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-graphical-gnome-macbook.nix default.nix
+</programlisting>
+  </section>
   <section xml:id="sec-building-image-tech-notes">
     <title>Technical Notes</title>
     <para>
diff --git a/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml b/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml
index 525531a478135..024a24379dd6d 100644
--- a/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml
+++ b/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml
@@ -248,7 +248,7 @@ $ nix-env -p /nix/var/nix/profiles/system -f '&lt;nixpkgs/nixos&gt;' -I nixos-co
         (since your Nix install was probably single user):
       </para>
       <programlisting>
-$ sudo chown -R 0.0 /nix
+$ sudo chown -R 0:0 /nix
 </programlisting>
     </listitem>
     <listitem>
diff --git a/nixos/doc/manual/from_md/installation/installing-kexec.section.xml b/nixos/doc/manual/from_md/installation/installing-kexec.section.xml
new file mode 100644
index 0000000000000..46ea0d59b6c30
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-kexec.section.xml
@@ -0,0 +1,94 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-booting-via-kexec">
+  <title><quote>Booting</quote> into NixOS via kexec</title>
+  <para>
+    In some cases, your system might already be booted into/preinstalled
+    with another Linux distribution, and booting NixOS by attaching an
+    installation image is quite a manual process.
+  </para>
+  <para>
+    This is particularly useful for (cloud) providers where you can’t
+    boot a custom image, but get some Debian or Ubuntu installation.
+  </para>
+  <para>
+    In these cases, it might be easier to use <literal>kexec</literal>
+    to <quote>jump into NixOS</quote> from the running system, which
+    only assumes <literal>bash</literal> and <literal>kexec</literal> to
+    be installed on the machine.
+  </para>
+  <para>
+    Note that kexec may not work correctly on some hardware, as devices
+    are not fully re-initialized in the process. In practice, this
+    however is rarely the case.
+  </para>
+  <para>
+    To build the necessary files from your current version of nixpkgs,
+    you can run:
+  </para>
+  <programlisting>
+nix-build -A kexec.x86_64-linux '&lt;nixpkgs/nixos/release.nix&gt;'
+</programlisting>
+  <para>
+    This will create a <literal>result</literal> directory containing
+    the following:
+  </para>
+  <itemizedlist spacing="compact">
+    <listitem>
+      <para>
+        <literal>bzImage</literal> (the Linux kernel)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        <literal>initrd</literal> (the initrd file)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        <literal>kexec-boot</literal> (a shellscript invoking
+        <literal>kexec</literal>)
+      </para>
+    </listitem>
+  </itemizedlist>
+  <para>
+    These three files are meant to be copied over to the other already
+    running Linux Distribution.
+  </para>
+  <para>
+    Note it’s symlinks pointing elsewhere, so <literal>cd</literal> in,
+    and use <literal>scp * root@$destination</literal> to copy it over,
+    rather than rsync.
+  </para>
+  <para>
+    Once you finished copying, execute <literal>kexec-boot</literal>
+    <emphasis>on the destination</emphasis>, and after some seconds, the
+    machine should be booting into an (ephemeral) NixOS installation
+    medium.
+  </para>
+  <para>
+    In case you want to describe your own system closure to kexec into,
+    instead of the default installer image, you can build your own
+    <literal>configuration.nix</literal>:
+  </para>
+  <programlisting language="bash">
+{ modulesPath, ... }: {
+  imports = [
+    (modulesPath + &quot;/installer/netboot/netboot-minimal.nix&quot;)
+  ];
+
+  services.openssh.enable = true;
+  users.users.root.openssh.authorizedKeys.keys = [
+    &quot;my-ssh-pubkey&quot;
+  ];
+}
+</programlisting>
+  <programlisting>
+nix-build '&lt;nixpkgs/nixos&gt;' \
+  --arg configuration ./configuration.nix
+  --attr config.system.build.kexecTree
+</programlisting>
+  <para>
+    Make sure your <literal>configuration.nix</literal> does still
+    import <literal>netboot-minimal.nix</literal> (or
+    <literal>netboot-base.nix</literal>).
+  </para>
+</section>
diff --git a/nixos/doc/manual/from_md/installation/installing-pxe.section.xml b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
index 1dd15ddacba86..94172de65ea06 100644
--- a/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
+++ b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
@@ -7,11 +7,11 @@
   <para>
     These instructions assume that you have an existing PXE or iPXE
     infrastructure and simply want to add the NixOS installer as another
-    option. To build the necessary files from a recent version of
+    option. To build the necessary files from your current version of
     nixpkgs, you can run:
   </para>
   <programlisting>
-nix-build -A netboot.x86_64-linux nixos/release.nix
+nix-build -A netboot.x86_64-linux '&lt;nixpkgs/nixos/release.nix&gt;'
 </programlisting>
   <para>
     This will create a <literal>result</literal> directory containing: *
diff --git a/nixos/doc/manual/from_md/installation/installing-usb.section.xml b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
index b46a1d565557d..df266eb16800f 100644
--- a/nixos/doc/manual/from_md/installation/installing-usb.section.xml
+++ b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
@@ -17,7 +17,7 @@ $ diskutil list
 [..]
 $ diskutil unmountDisk diskN
 Unmount of all volumes on diskN was successful
-$ sudo dd if=nix.iso of=/dev/rdiskN
+$ sudo dd if=nix.iso of=/dev/rdiskN bs=1M
 </programlisting>
     <para>
       Using the 'raw' <literal>rdiskN</literal> device instead of
diff --git a/nixos/doc/manual/from_md/installation/installing.chapter.xml b/nixos/doc/manual/from_md/installation/installing.chapter.xml
index db073fa839653..19ff841f5a679 100644
--- a/nixos/doc/manual/from_md/installation/installing.chapter.xml
+++ b/nixos/doc/manual/from_md/installation/installing.chapter.xml
@@ -411,6 +411,13 @@ OK
                 specify on which disk the GRUB boot loader is to be
                 installed. Without it, NixOS cannot boot.
               </para>
+              <para>
+                If there are other operating systems running on the
+                machine before installing NixOS, the
+                <xref linkend="opt-boot.loader.grub.useOSProber" />
+                option can be set to <literal>true</literal> to
+                automatically add them to the grub menu.
+              </para>
             </listitem>
           </varlistentry>
           <varlistentry>
@@ -437,13 +444,6 @@ OK
           </varlistentry>
         </variablelist>
         <para>
-          If there are other operating systems running on the machine
-          before installing NixOS, the
-          <xref linkend="opt-boot.loader.grub.useOSProber" /> option can
-          be set to <literal>true</literal> to automatically add them to
-          the grub menu.
-        </para>
-        <para>
           If you need to configure networking for your machine the
           configuration options are described in
           <xref linkend="sec-networking" />. In particular, while wifi
@@ -638,6 +638,7 @@ $ passwd eelco
     <title>Additional installation notes</title>
     <xi:include href="installing-usb.section.xml" />
     <xi:include href="installing-pxe.section.xml" />
+    <xi:include href="installing-kexec.section.xml" />
     <xi:include href="installing-virtualbox-guest.section.xml" />
     <xi:include href="installing-from-other-distro.section.xml" />
     <xi:include href="installing-behind-a-proxy.section.xml" />
diff --git a/nixos/doc/manual/from_md/installation/upgrading.chapter.xml b/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
index e3b77d4c3650b..11fe1d317ccdd 100644
--- a/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
+++ b/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
@@ -12,7 +12,7 @@
     <listitem>
       <para>
         <emphasis>Stable channels</emphasis>, such as
-        <link xlink:href="https://nixos.org/channels/nixos-21.11"><literal>nixos-21.11</literal></link>.
+        <link xlink:href="https://nixos.org/channels/nixos-22.05"><literal>nixos-22.05</literal></link>.
         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),
@@ -33,7 +33,7 @@
     <listitem>
       <para>
         <emphasis>Small channels</emphasis>, such as
-        <link xlink:href="https://nixos.org/channels/nixos-21.11-small"><literal>nixos-21.11-small</literal></link>
+        <link xlink:href="https://nixos.org/channels/nixos-22.05-small"><literal>nixos-22.05-small</literal></link>
         or
         <link xlink:href="https://nixos.org/channels/nixos-unstable-small"><literal>nixos-unstable-small</literal></link>.
         These are identical to the stable and unstable channels
@@ -60,8 +60,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 21.11 ISO, you will be subscribed
-    to the <literal>nixos-21.11</literal> channel. To see which NixOS
+    instance, if you installed from a 22.05 ISO, you will be subscribed
+    to the <literal>nixos-22.05</literal> channel. To see which NixOS
     channel you’re subscribed to, run the following as root:
   </para>
   <programlisting>
@@ -76,17 +76,17 @@ nixos https://nixos.org/channels/nixos-unstable
 </programlisting>
   <para>
     (Be sure to include the <literal>nixos</literal> parameter at the
-    end.) For instance, to use the NixOS 21.11 stable channel:
+    end.) For instance, to use the NixOS 22.05 stable channel:
   </para>
   <programlisting>
-# nix-channel --add https://nixos.org/channels/nixos-21.11 nixos
+# nix-channel --add https://nixos.org/channels/nixos-22.05 nixos
 </programlisting>
   <para>
     If you have a server, you may want to use the <quote>small</quote>
     channel instead:
   </para>
   <programlisting>
-# nix-channel --add https://nixos.org/channels/nixos-21.11-small nixos
+# nix-channel --add https://nixos.org/channels/nixos-22.05-small nixos
 </programlisting>
   <para>
     And if you want to live on the bleeding edge:
@@ -146,7 +146,7 @@ system.autoUpgrade.allowReboot = true;
       also specify a channel explicitly, e.g.
     </para>
     <programlisting language="bash">
-system.autoUpgrade.channel = https://nixos.org/channels/nixos-21.11;
+system.autoUpgrade.channel = https://nixos.org/channels/nixos-22.05;
 </programlisting>
   </section>
 </chapter>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml
index f54f6129e0db9..910cad467e9d8 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml
@@ -866,6 +866,14 @@
           package.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          The vim/kakoune plugin updater now reads from a CSV file:
+          check
+          <literal>pkgs/applications/editors/vim/plugins/vim-plugin-names</literal>
+          out to see the new format
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 59da373f38e17..d9ebbe74d54fa 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -26,8 +26,36 @@
       </listitem>
       <listitem>
         <para>
-          <literal>iptables</literal> now uses
-          <literal>nf_tables</literal> backend.
+          <literal>iptables</literal> is now using
+          <literal>nf_tables</literal> under the hood, by using
+          <literal>iptables-nft</literal>, similar to
+          <link xlink:href="https://wiki.debian.org/nftables#Current_status">Debian</link>
+          and
+          <link xlink:href="https://fedoraproject.org/wiki/Changes/iptables-nft-default">Fedora</link>.
+          This means, <literal>ip[6]tables</literal>,
+          <literal>arptables</literal> and <literal>ebtables</literal>
+          commands will actually show rules from some specific tables in
+          the <literal>nf_tables</literal> kernel subsystem. In case
+          you’re migrating from an older release without rebooting,
+          there might be cases where you end up with iptable rules
+          configured both in the legacy <literal>iptables</literal>
+          kernel backend, as well as in the <literal>nf_tables</literal>
+          backend. This can lead to confusing firewall behaviour. An
+          <literal>iptables-save</literal> after switching will complain
+          about <quote>iptables-legacy tables present</quote>. It’s
+          probably best to reboot after the upgrade, or manually
+          removing all legacy iptables rules (via the
+          <literal>iptables-legacy</literal> package).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          systemd got an <literal>nftables</literal> backend, and
+          configures (networkd) rules in their own
+          <literal>io.systemd.*</literal> tables. Check
+          <literal>nft list ruleset</literal> to see these rules, not
+          <literal>iptables-save</literal> (which only shows
+          <literal>iptables</literal>-created rules.
         </para>
       </listitem>
       <listitem>
@@ -541,8 +569,9 @@
       <listitem>
         <para>
           The NixOS VM test framework,
-          <literal>pkgs.nixosTest</literal>/<literal>make-test-python.nix</literal>,
-          now requires detaching commands such as
+          <literal>pkgs.nixosTest</literal>/<literal>make-test-python.nix</literal>
+          (<literal>pkgs.testers.nixosTest</literal> since 22.05), now
+          requires detaching commands such as
           <literal>succeed(&quot;foo &amp;&quot;)</literal> and
           <literal>succeed(&quot;foo | xclip -i&quot;)</literal> to
           close stdout. This can be done with a redirect such as
@@ -1429,6 +1458,17 @@ Superuser created successfully.
           knob.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>/usr</literal> will always be included in the initial
+          ramdisk. See the
+          <literal>fileSystems.&lt;name&gt;.neededForBoot</literal>
+          option. If any files exist under <literal>/usr</literal>
+          (which is not typical for NixOS), they will be included in the
+          initial ramdisk, increasing its size to a possibly problematic
+          extent.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-notable-changes">
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 544b1e138989b..245250e709147 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -1,9 +1,5 @@
 <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-22.05">
-  <title>Release 22.05 (“Quokka”, 2022.05/??)</title>
-  <para>
-    In addition to numerous new and upgraded packages, this release has
-    the following highlights:
-  </para>
+  <title>Release 22.05 (“Quokka”, 2022.05/30)</title>
   <itemizedlist spacing="compact">
     <listitem>
       <para>
@@ -14,32 +10,103 @@
   </itemizedlist>
   <section xml:id="sec-release-22.05-highlights">
     <title>Highlights</title>
+    <para>
+      In addition to numerous new and upgraded packages, this release
+      has the following highlights:
+    </para>
     <itemizedlist>
       <listitem>
+<literallayout>Nix has been updated from 2.3 to 2.8. This mainly brings experimental support for Flakes, but also marks the <literal>nix</literal> command as experimental which now has to be enabled via the configuration explicitly. For more information and instructions for upgrades, see the relase notes for <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.4.html">nix-2.4</link>,
+<link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.5.html">nix-2.5</link>, <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.6.html">nix-2.6</link>, <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.7.html">nix-2.7</link> and <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.8.html">nix-2.8</link></literallayout>
+      </listitem>
+      <listitem>
         <para>
-          <literal>security.acme.defaults</literal> has been added to
-          simplify configuring settings for many certificates at once.
-          This also opens up the the option to use DNS-01 validation
-          when using <literal>enableACME</literal> on web server virtual
-          hosts (e.g.
-          <literal>services.nginx.virtualHosts.*.enableACME</literal>).
+          The <literal>firefox</literal> browser on
+          <literal>x86_64-linux</literal> now makes use of
+          profile-guided optimisation, resulting in a much more
+          responsive browsing experience.
         </para>
       </listitem>
       <listitem>
         <para>
-          PHP 8.1 is now available
+          GNOME has been upgraded to 42. Please take a look at their
+          <link xlink:href="https://release.gnome.org/42/">Release
+          Notes</link> for details. In particular, it replaces gedit
+          with GNOME Text Editor, GNOME Terminal with GNOME Console
+          (formerly King’s Cross) and GNOME Screenshot by a tool
+          integrated into the Shell.
         </para>
       </listitem>
       <listitem>
         <para>
-          Mattermost has been updated to extended support release 6.3,
-          as the previously packaged extended support release 5.37 is
-          <link xlink:href="https://docs.mattermost.com/upgrade/extended-support-release.html">reaching
-          its end of life</link>. Migrations may take a while, see the
-          <link xlink:href="https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release">changelog</link>
-          and
-          <link xlink:href="https://docs.mattermost.com/upgrade/important-upgrade-notes.html">important
-          upgrade notes</link>.
+          PHP 8.1 is now available.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          systemd services can now set
+          <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadTriggers</link>
+          instead of <literal>reloadIfChanged</literal> for a more
+          granular distinction between reloads and restarts.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Systemd has been upgraded to the version 250.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Pulseaudio has been updated to version 15.0 and now optionally
+          <link xlink:href="https://www.freedesktop.org/wiki/Software/PulseAudio/Notes/15.0/#supportforldacandaptxbluetoothcodecsplussbcxqsbcwithhigher-qualityparameters">supports
+          additional Bluetooth audio codecs</link> such as aptX or LDAC,
+          with codec switching available in
+          <literal>pavucontrol</literal>. This feature is disabled by
+          default, but can be enabled with the option
+          <literal>hardware.pulseaudio.package = pkgs.pulseaudioFull;</literal>.
+          Existing third-party modules that offered similar functions,
+          such as <literal>pulseaudio-modules-bt</literal> or
+          <literal>pulseaudio-hsphfpd</literal>, are obsolete and have
+          been removed.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PostgreSQL now defaults to major version 14.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Module authors can use
+          <literal>mkRenamedOptionModuleWith</literal> to automate the
+          deprecation cycle without annoying out-of-tree module authors
+          and their users.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The default GHC version has been updated from 8.10.7 to 9.0.2.
+          <literal>pkgs.haskellPackages</literal> and
+          <literal>pkgs.ghc</literal> will now use this version by
+          default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The GNOME and Plasma installation CDs now use
+          <literal>pkgs.calamares</literal> and
+          <literal>pkgs.calamares-nixos-extensions</literal> to allow
+          users to easily install and set up NixOS with a GUI.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>security.acme.defaults</literal> has been added to
+          simplify the configuration of settings for many certificates
+          at once. This also opens up the option to use DNS-01
+          validation when using <literal>enableACME</literal> web server
+          virtual hosts (e.g.
+          <literal>services.nginx.virtualHosts.*.enableACME</literal>).
         </para>
       </listitem>
     </itemizedlist>
@@ -49,6 +116,16 @@
     <itemizedlist>
       <listitem>
         <para>
+          <link xlink:href="https://1password.com/">1password</link>,
+          command-lines and graphic interface for 1Password. Available
+          as
+          <link linkend="opt-programs._1password.enable">programs._1password</link>
+          and
+          <link linkend="opt-programs._1password.enable">programs._1password-gui</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw">aesmd</link>,
           the Intel SGX Architectural Enclave Service Manager. Available
           as
@@ -57,18 +134,101 @@
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://docs.docker.com/engine/security/rootless/">rootless
-          Docker</link>, a <literal>systemd --user</literal> Docker
-          service which runs without root permissions. Available as
-          <link xlink:href="options.html#opt-virtualisation.docker.rootless.enable">virtualisation.docker.rootless.enable</link>.
+          <link xlink:href="https://github.com/mbrubeck/agate">agate</link>,
+          a very simple server for the Gemini hypertext protocol.
+          Available as
+          <link linkend="opt-services.agate.enable">services.agate</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://conduit.rs/">matrix-conduit</link>,
-          a simple, fast and reliable chat server powered by matrix.
-          Available as
-          <link xlink:href="option.html#opt-services.matrix-conduit.enable">services.matrix-conduit</link>.
+          <link xlink:href="https://github.com/linux-apfs/linux-apfs-rw">apfs</link>,
+          a kernel module for mounting the Apple File System (APFS).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://gitlab.com/DarkElvenAngel/argononed">argonone</link>,
+          a replacement daemon for the Raspberry Pi Argon One power
+          button and cooler. Available at
+          <link xlink:href="options.html#opt-services.hardware.argonone.enable">services.hardware.argonone</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm">ArchiSteamFarm</link>,
+          a C# application with primary purpose of idling Steam cards
+          from multiple accounts simultaneously. Available as
+          <link linkend="opt-services.archisteamfarm.enable">services.archisteamfarm</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://loic-sharma.github.io/BaGet/">BaGet</link>,
+          a lightweight NuGet and symbol server. Available at
+          <link linkend="opt-services.baget.enable">services.baget</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/xddxdd/bird-lg-go">bird-lg</link>,
+          a BGP looking glass for Bird Routing. Available as
+          <link linkend="opt-services.bird-lg.package">services.bird-lg</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://0xerr0r.github.io/blocky/">blocky</link>,
+          fast and lightweight DNS proxy as ad-blocker for local network
+          with many features. Available as
+          <link linkend="opt-services.blocky.enable">services.blocky</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/kissgyorgy/cloudflare-dyndns">cloudflare-dyndns</link>,
+          CloudFlare Dynamic DNS client. Available as
+          <link linkend="opt-services.cloudflare-dyndns.enable">services.cloudflare-dyndns</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://corosync.github.io/corosync/">Corosync</link>
+          and
+          <link xlink:href="https://clusterlabs.org/pacemaker/">Pacemaker</link>,
+          A open-source high availability resource manager. Available as
+          <link linkend="opt-services.corosync.enable">services.corosync</link>
+          and
+          <link linkend="opt-services.pacemaker.enable">services.pacemaker</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/lakinduakash/linux-wifi-hotspot">create_ap</link>,
+          a module for creating wifi hotspots using the program
+          linux-wifi-hotspot. Available as
+          <link linkend="opt-services.create_ap.enable">services.create_ap</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.envoyproxy.io/">Envoy</link>, a
+          high-performance reverse proxy. Available as
+          <link linkend="opt-services.envoy.enable">services.envoy</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://ergo.chat">ergochat</link>, a modern
+          IRC with IRCv3 features. Available as
+          <link linkend="opt-services.ergochat.enable">services.ergochat</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/audreyt/ethercalc">ethercalc</link>,
+          an online collaborative spreadsheet. Available as
+          <link linkend="opt-services.ethercalc.enable">services.ethercalc</link>.
         </para>
       </listitem>
       <listitem>
@@ -81,37 +241,57 @@
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://github.com/linux-apfs/linux-apfs-rw">apfs</link>,
-          a kernel module for mounting the Apple File System (APFS).
+          <link xlink:href="https://frrouting.org/">FRRouting</link>, a
+          popular suite of Internet routing protocol daemons (BGP, BFD,
+          OSPF, IS-IS, VRRP and others). Available as
+          <link linkend="opt-services.frr.babel.enable">services.frr</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://frrouting.org/">FRRouting</link>, a
-          popular suite of Internet routing protocol daemons (BGP, BFD,
-          OSPF, IS-IS, VVRP and others). Available as
-          <link linkend="opt-services.ffr.babel.enable">services.frr</link>
+          <link xlink:href="https://grafana.com/oss/mimir/">Grafana
+          Mimir</link>, an open source, horizontally scalable, highly
+          available, multi-tenant, long-term storage for Prometheus.
+          Available as
+          <link linkend="opt-services.mimir.enable">services.mimir</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://hastebin.com/about.md">Haste</link>,
+          a pastebin written in node.js. Available as
+          <link linkend="opt-services.haste-server.enable">services.haste</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/juanfont/headscale">headscale</link>,
+          an Open Source implementation of the
+          <link xlink:href="https://tailscale.io">Tailscale</link>
+          Control Server. Available as
+          <link linkend="opt-services.headscale.enable">services.headscale</link>.
         </para>
       </listitem>
       <listitem>
         <para>
           <link xlink:href="https://github.com/hifi/heisenbridge">heisenbridge</link>,
           a bouncer-style Matrix IRC bridge. Available as
-          <link xlink:href="options.html#opt-services.heisenbridge.enable">services.heisenbridge</link>.
+          <link linkend="opt-services.heisenbridge.enable">services.heisenbridge</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://ergo.chat">ergochat</link>, a modern
-          IRC with IRCv3 features. Available as
-          <link xlink:href="options.html#opt-services.ergochat.enable">services.ergochat</link>.
+          <link xlink:href="https://github.com/aarond10/https_dns_proxy">https-dns-proxy</link>,
+          DNS to DNS over HTTPS (DoH) proxy. Available as
+          <link linkend="opt-services.https-dns-proxy.enable">services.https-dns-proxy</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</link>,
-          a web interface for the PowerDNS server. Available at
-          <link xlink:href="options.html#opt-services.powerdns-admin.enable">services.powerdns-admin</link>.
+          <link xlink:href="https://github.com/sezanzeb/input-remapper">input-remapper</link>,
+          an easy to use tool to change the mapping of your input device
+          buttons. Available at
+          <link linkend="opt-services.input-remapper.enable">services.input-remapper</link>.
         </para>
       </listitem>
       <listitem>
@@ -119,60 +299,133 @@
           <link xlink:href="https://invoiceplane.com">InvoicePlane</link>,
           web application for managing and creating invoices. Available
           at
-          <link xlink:href="options.html#opt-services.invoiceplane.enable">services.invoiceplane</link>.
+          <link linkend="opt-services.invoiceplane.sites._name_.enable">services.invoiceplane</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://maddy.email">maddy</link>, a
-          composable all-in-one mail server. Available as
-          <link xlink:href="options.html#opt-services.maddy.enable">services.maddy</link>.
+          <link xlink:href="https://userbase.kde.org/K3b">k3b</link>,
+          the KDE disk burning application. Available as
+          <link linkend="opt-programs.k3b.enable">programs.k3b</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.scorchworks.com/K40whisperer/k40whisperer.html">K40-Whisperer</link>,
+          a program to control cheap Chinese laser cutters. Available as
+          <link linkend="opt-programs.k40-whisperer.enable">programs.k40-whisperer.enable</link>.
+          Users must add themselves to the <literal>k40</literal> group
+          to be able to access the device.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://kanidm.github.io/kanidm/stable/">kanidm</link>,
+          an identity management server written in Rust. Available as
+          <link linkend="opt-services.kanidm.enableServer">services.kanidm</link>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://maddy.email/">Maddy</link>, a free
+          an open source mail server. Availabe as
+          <link linkend="opt-services.maddy.enable">services.maddy</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://conduit.rs/">matrix-conduit</link>,
+          a simple, fast and reliable chat server powered by matrix.
+          Available as
+          <link xlink:href="option.html#opt-services.matrix-conduit.enable">services.matrix-conduit</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://moosefs.com">Moosefs</link>, fault
+          tolerant petabyte distributed file system. Available as
+          <link linkend="opt-services.moosefs.master.enable">moosefs</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/mozilla-mobile/mozilla-vpn-client">mozillavpn</link>,
+          the client for the
+          <link xlink:href="https://vpn.mozilla.org/">Mozilla VPN</link>
+          service. Available as
+          <link linkend="opt-services.mozillavpn.enable">services.mozillavpn</link>.
         </para>
       </listitem>
       <listitem>
         <para>
           <link xlink:href="https://github.com/mgumz/mtr-exporter">mtr-exporter</link>,
           a Prometheus exporter for mtr metrics. Available as
-          <link xlink:href="options.html#opt-services.mtr-exporter.enable">services.mtr-exporter</link>.
+          <link linkend="opt-services.mtr-exporter.enable">services.mtr-exporter</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://tetrd.app">tetrd</link>, share your
-          internet connection from your device to your PC and vice versa
-          through a USB cable. Available at
-          <link linkend="opt-services.tetrd.enable">services.tetrd</link>.
+          <link xlink:href="https://nbd.sourceforge.io/">nbd</link>, a
+          Network Block Device server. Available as
+          <link linkend="opt-services.nbd.server.enable">services.nbd</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm">ArchiSteamFarm</link>,
-          a C# application with primary purpose of idling Steam cards
-          from multiple accounts simultaneously. Available as
-          <link xlink:href="options.html#opt-services.archisteamfarm.enable">services.archisteamfarm</link>.
+          <link xlink:href="https://github.com/netbox-community/netbox">netbox</link>,
+          infrastructure resource modeling (IRM) tool. Available as
+          <link linkend="opt-services.netbox.enable">services.netbox</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://goteleport.com">teleport</link>,
-          allows engineers and security professionals to unify access
-          for SSH servers, Kubernetes clusters, web applications, and
-          databases across all environments. Available at
-          <link linkend="opt-services.teleport.enable">services.teleport</link>.
+          <link xlink:href="https://github.com/vvilhonen/nethoscope">nethoscope</link>,
+          listen to your network traffic. Available as
+          <link linkend="opt-programs.nethoscope.enable">programs.nethoscope</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://loic-sharma.github.io/BaGet/">BaGet</link>,
-          a lightweight NuGet and symbol server. Available at
-          <link linkend="opt-services.baget.enable">services.baget</link>.
+          <link xlink:href="https://nifi.apache.org">nifi</link>, an
+          easy to use, powerful, and reliable system to process and
+          distribute data. Available as
+          <link linkend="opt-services.nifi.enable">services.nifi</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://moosefs.com">moosefs</link>, fault
-          tolerant petabyte distributed file system. Available as
-          <link linkend="opt-services.moosefs">moosefs</link>.
+          <link xlink:href="https://github.com/Mic92/nix-ld">nix-ld</link>,
+          Run unpatched dynamic binaries on NixOS. Available as
+          <link linkend="opt-programs.nix-ld.enable">programs.nix-ld</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="http://www.nncpgo.org">NNCP</link>, NNCP
+          (Node to Node copy) utilities and configuration, Available as
+          <link linkend="opt-programs.nncp.enable">programs.nncp</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/postgres/pgadmin4">pgadmin4</link>,
+          an admin interface for the PostgreSQL database. Available at
+          <link linkend="opt-services.pgadmin.enable">services.pgadmin</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</link>,
+          a web interface for the PowerDNS server. Available at
+          <link linkend="opt-services.powerdns-admin.enable">services.powerdns-admin</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/prometheus-pve/prometheus-pve-exporter">prometheus-pve-exporter</link>,
+          a tool that exposes information from the Proxmox VE API for
+          use by Prometheus. Available as
+          <link linkend="opt-services.prometheus.exporters.pve.enable">services.prometheus.exporters.pve</link>.
         </para>
       </listitem>
       <listitem>
@@ -184,17 +437,33 @@
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://github.com/audreyt/ethercalc">ethercalc</link>,
-          an online collaborative spreadsheet. Available as
-          <link xlink:href="options.html#opt-services.ethercalc.enable">services.ethercalc</link>.
+          <link xlink:href="https://public-inbox.org">Public
+          Inbox</link>, an <quote>archives first</quote> approach to
+          mailing lists. Available as
+          <link linkend="opt-services.public-inbox.enable">services.public-inbox</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/fleaz/r53-ddns">r53-ddns</link>,
+          a small tool to run your own DDNS service via AWS Route53.
+          Available as
+          <link linkend="opt-services.r53-ddns.enable">services.r53-ddns</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://timetagger.app">timetagger</link>,
-          an open source time-tracker with an intuitive user experience
-          and powerful reporting.
-          <link xlink:href="options.html#opt-services.timetagger.enable">services.timetagger</link>.
+          <link xlink:href="https://ddvk.github.io/rmfakecloud/">rmfakecloud</link>,
+          a clone of the cloud sync the remarkable tablet. Available as
+          <link linkend="opt-services.rmfakecloud.enable">services.rmfakecloud</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://docs.docker.com/engine/security/rootless/">rootless
+          Docker</link>, a <literal>systemd --user</literal> Docker
+          service which runs without root permissions. Available as
+          <link linkend="opt-virtualisation.docker.rootless.enable">virtualisation.docker.rootless.enable</link>.
         </para>
       </listitem>
       <listitem>
@@ -202,16 +471,111 @@
           <link xlink:href="https://www.rstudio.com/products/rstudio/#rstudio-server">rstudio-server</link>,
           a browser-based version of the RStudio IDE for the R
           programming language. Available as
-          <link xlink:href="options.html#opt-services.rstudio-server.enable">services.rstudio-server</link>.
+          <link linkend="opt-services.rstudio-server.enable">services.rstudio-server</link>.
         </para>
       </listitem>
       <listitem>
         <para>
-          <link xlink:href="https://github.com/juanfont/headscale">headscale</link>,
-          an Open Source implementation of the
-          <link xlink:href="https://tailscale.io">Tailscale</link>
-          Control Server. Available as
-          <link xlink:href="options.html#opt-services.headscale.enable">services.headscale</link>
+          <link xlink:href="https://github.com/aler9/rtsp-simple-server">rtsp-simple-server</link>,
+          ready-to-use RTSP / RTMP / HLS server and proxy that allows to
+          read, publish and proxy video and audio streams. Available as
+          <link linkend="opt-services.rtsp-simple-server.enable">services.rtsp-simple-server</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://snipeitapp.com">Snipe-IT</link>, a
+          free open source IT asset/license management system. Available
+          as
+          <link linkend="opt-services.snipe-it.enable">services.snipe-it</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://snowflake.torproject.org/">snowflake-proxy</link>,
+          a system to defeat internet censorship. Available as
+          <link linkend="opt-services.snowflake-proxy.enable">services.snowflake-proxy</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://sslmate.com/">sslmate-agent</link>,
+          a daemon for managing SSL/TLS certificates on a server.
+          Available as
+          <link xlink:href="services.sslmate-agent.enable">services.sslmate-agent</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://starship.rs">starship</link>, a
+          minimal, blazing-fast, and infinitely customizable prompt for
+          any shell. Available at
+          <link linkend="opt-programs.starship.enable">programs.startship</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/rfjakob/systembus-notify">systembus-notify</link>,
+          allow system level notifications to reach the users. Available
+          as
+          <link xlink:href="opt-services.systembus-notify.enable">services.systembus-notify</link>.
+          Please keep in mind that this service should only be enabled
+          on machines with fully trusted users, as any local user is
+          able to DoS user sessions by spamming notifications.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://goteleport.com">teleport</link>,
+          allows engineers and security professionals to unify access
+          for SSH servers, Kubernetes clusters, web applications, and
+          databases across all environments. Available at
+          <link linkend="opt-services.teleport.enable">services.teleport</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://tetrd.app">tetrd</link>, share your
+          internet connection from your device to your PC and vice versa
+          through a USB cable. Available at
+          <link linkend="opt-services.tetrd.enable">services.tetrd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://upterm.dev">uptermd</link>, an
+          open-source solution for sharing terminal sessions instantly
+          over the public internet via secure tunnels. Available at
+          <link linkend="opt-services.uptermd.enable">services.uptermd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/darrylb123/usbrelay">usbrelayd</link>,
+          an USB Relay MQTT daemon. Available as
+          <link linkend="opt-services.usbrelayd.enable">services.usbrelayd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/miquels/webdav-server-rs">webdav-server-rs</link>,
+          Webdav server in rust. Available as
+          <link linkend="opt-services.webdav-server-rs.enable">services.webdav-server-rs</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/gin66/wg_netmanager">wg-netmanager</link>,
+          the Wireguard network manager. Available as
+          <link linkend="opt-services.wg-netmanager.enable">services.wg-netmanager</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://zammad.org/">Zammad</link>, a
+          web-based, open source user support/ticketing solution.
+          Available as
+          <link linkend="opt-services.zammad.enable">services.zammad</link>.
         </para>
       </listitem>
     </itemizedlist>
@@ -255,6 +619,32 @@
       </listitem>
       <listitem>
         <para>
+          The update of the haskell package set brings with it a new
+          version of the <literal>xmonad</literal> module, which will
+          break your configuration if you use <literal>launch</literal>
+          as entrypoint. The example code the corresponding nixos module
+          was adjusted, you may want to have a look at it.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>home-assistant</literal> module now requires
+          users that don’t want their configuration to be managed
+          declaratively to set
+          <literal>services.home-assistant.config = null;</literal>.
+          This is required due to the way default settings are handled
+          with the new settings style.
+        </para>
+        <para>
+          Additionally the default list of
+          <literal>extraComponents</literal> now includes the minimal
+          dependencies to successfully complete the
+          <link xlink:href="https://www.home-assistant.io/getting-started/onboarding/">onboarding</link>
+          procedure.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.emacsPackages.orgPackages</literal> is removed
           because org elpa is deprecated. The packages in the top level
           of <literal>pkgs.emacsPackages</literal>, such as org and
@@ -266,12 +656,216 @@
       </listitem>
       <listitem>
         <para>
+          The configuration and state directories used by
+          <literal>nixos-containers</literal> have been moved from
+          <literal>/etc/containers</literal> and
+          <literal>/var/lib/containers</literal> to
+          <literal>/etc/nixos-containers</literal> and
+          <literal>/var/lib/nixos-containers</literal>.
+        </para>
+        <para>
+          If you are changing <literal>system.stateVersion</literal> to
+          <literal>&quot;22.05&quot;</literal> manually on an existing
+          system you are responsible for migrating these directories
+          yourself.
+        </para>
+        <para>
+          This is to improve compatibility with
+          <literal>libcontainer</literal> based software such as Podman
+          and Skopeo which assumes they have ownership over
+          <literal>/etc/containers</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>lib.systems.supported</literal> has been removed, as
+          it was overengineered for determining the systems to support
+          in the nixpkgs flake. The list of systems exposed by the
+          nixpkgs flake can now be accessed as
+          <literal>lib.systems.flakeExposed</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          For new installations
+          <literal>virtualisation.oci-containers.backend</literal> is
+          now set to <literal>podman</literal> by default. If you still
+          want to use Docker on systems where
+          <literal>system.stateVersion</literal> is set to to
+          <literal>&quot;22.05&quot;</literal> set
+          <literal>virtualisation.oci-containers.backend = &quot;docker&quot;;</literal>.Old
+          systems with older <literal>stateVersion</literal>s stay with
+          <quote>docker</quote>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>security.klogd</literal> was removed. Logging of
+          kernel messages is handled by systemd since Linux 3.5.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.ssmtp</literal> has been dropped due to the
+          program being unmaintained. <literal>pkgs.msmtp</literal> can
+          be used instead as a substitute <literal>sendmail</literal>
+          implementation. The corresponding options
+          <literal>services.ssmtp.*</literal> have been removed as well.
+          <literal>programs.msmtp.*</literal> can be used instead for an
+          equivalent setup. For example:
+        </para>
+        <programlisting language="bash">
+{
+  # Original ssmtp configuration:
+  services.ssmtp = {
+    enable = true;
+    useTLS = true;
+    useSTARTTLS = true;
+    hostName = &quot;smtp.example:587&quot;;
+    authUser = &quot;someone&quot;;
+    authPassFile = &quot;/secrets/password.txt&quot;;
+  };
+
+  # Equivalent msmtp configuration:
+  programs.msmtp = {
+    enable = true;
+    accounts.default = {
+      tls = true;
+      tls_starttls = true;
+      auth = true;
+      host = &quot;smtp.example&quot;;
+      port = 587;
+      user = &quot;someone&quot;;
+      passwordeval = &quot;cat /secrets/password.txt&quot;;
+    };
+  };
+}
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
           <literal>services.kubernetes.addons.dashboard</literal> was
           removed due to it being an outdated version.
         </para>
       </listitem>
       <listitem>
         <para>
+          <literal>services.kubernetes.scheduler.{port,address}</literal>
+          now set <literal>--secure-port</literal> and
+          <literal>--bind-address</literal> instead of
+          <literal>--port</literal> and <literal>--address</literal>,
+          since the former have been deprecated and are no longer
+          functional in kubernetes&gt;=1.23. Ensure that you are not
+          relying on the insecure behaviour before upgrading.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          In the PowerDNS Recursor module
+          (<literal>services.pdns-recursor</literal>), default values of
+          several IP address-related NixOS options have been updated to
+          match the default upstream behavior. In particular, Recursor
+          by default will:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              listen on (and allows connections from) both IPv4 and IPv6
+              addresses
+              (<literal>services.pdns-recursor.dns.address</literal>,
+              <literal>services.pdns-recursor.dns.allowFrom</literal>);
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              allow only local connections to the REST API server
+              (<literal>services.pdns-recursor.api.allowFrom</literal>).
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          In the ncdns module, the default value of
+          <literal>services.ncdns.address</literal> has been changed to
+          the IPv6 loopback address (<literal>::1</literal>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>openldap</literal> (and therefore the slapd LDAP
+          server) were updated to version 2.6.2. The project introduced
+          backwards-incompatible changes, namely the removal of the bdb,
+          hdb, ndb, and shell backends in slapd. Therefore before
+          updating, dump your database <literal>slapcat -n 1</literal>
+          in LDIF format, and reimport it after updating your
+          <literal>services.openldap.settings</literal>, which
+          represents your <literal>cn=config</literal>.
+        </para>
+        <para>
+          Additionally with 2.5 the argon2 module was included in the
+          standard distrubtion and renamed from
+          <literal>pw-argon2</literal> to <literal>argon2</literal>.
+          Remember to update your <literal>olcModuleLoad</literal> entry
+          in <literal>cn=config</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>openssh</literal> has been update to 8.9p1, changing
+          the FIDO security key middleware interface.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>git</literal> no longer hardcodes the path to
+          openssh’ ssh binary to reduce the amount of rebuilds. If you
+          are using git with ssh remotes and do not have a ssh binary in
+          your enviroment consider adding <literal>openssh</literal> to
+          it or switching to <literal>gitFull</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.k3s.enable</literal> no longer implies
+          <literal>systemd.enableUnifiedCgroupHierarchy = false</literal>,
+          and will default to the <quote>systemd</quote> cgroup driver
+          when using <literal>services.k3s.docker = true</literal>. This
+          change may require a reboot to take effect, and k3s may not be
+          able to run if the boot cgroup hierarchy does not match its
+          configuration. The previous behavior may be retained by
+          explicitly setting
+          <literal>systemd.enableUnifiedCgroupHierarchy = false</literal>
+          in your configuration.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>fonts.fonts</literal> no longer includes ancient
+          bitmap fonts when both
+          <literal>config.services.xserver.enable</literal> and
+          <literal>config.nixpkgs.config.allowUnfree</literal> are
+          enabled. If you still want these fonts, use:
+        </para>
+        <programlisting language="bash">
+{
+  fonts.fonts = [
+    pkgs.xorg.fontbhlucidatypewriter100dpi
+    pkgs.xorg.fontbhlucidatypewriter75dpi
+    pkgs.xorg.fontbh100dpi
+  ];
+}
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.prometheus.alertManagerTimeout</literal> has
+          been removed as it has been deprecated upstream and has no
+          effect.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The DHCP server (<literal>services.dhcpd4</literal>,
           <literal>services.dhcpd6</literal>) has been hardened. The
           service is now using the systemd’s
@@ -296,6 +890,288 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.ipfs.extraFlags</literal> is now escaped
+          with <literal>utils.escapeSystemdExecArgs</literal>. If you
+          rely on systemd interpolating <literal>extraFlags</literal> in
+          the service <literal>ExecStart</literal>, this will no longer
+          work.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>hbase</literal> version 0.98.24 has been removed. The
+          package now defaults to version 2.4.11. Versions 1.7.1 and
+          3.0.0-alpha-2 are also available.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.paperless-ng</literal> was renamed to
+          <literal>services.paperless</literal>. Accordingly, the
+          <literal>paperless-ng-manage</literal> script (located in
+          <literal>dataDir</literal>) was renamed to
+          <literal>paperless-manage</literal>.
+          <literal>services.paperless</literal> now uses
+          <literal>paperless-ngx</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>matrix-synapse</literal> service
+          (<literal>services.matrix-synapse</literal>) has been
+          converted to use the <literal>settings</literal> option
+          defined in RFC42. This means that options that are part of
+          your <literal>homeserver.yaml</literal> configuration, and
+          that were specified at the top-level of the module
+          (<literal>services.matrix-synapse</literal>) now need to be
+          moved into
+          <literal>services.matrix-synapse.settings</literal>. And while
+          not all options you may use are defined in there, they are
+          still supported, because you can set arbitrary values in this
+          freeform type.
+        </para>
+        <para>
+          The <literal>listeners.*.bind_address</literal> option was
+          renamed to <literal>bind_addresses</literal> in order to match
+          the upstream <literal>homeserver.yaml</literal> option name.
+          It is now also a list of strings instead of a string.
+        </para>
+        <para>
+          An example to make the required migration clearer:
+        </para>
+        <para>
+          Before:
+        </para>
+        <programlisting language="bash">
+{
+  services.matrix-synapse = {
+    enable = true;
+
+    server_name = &quot;example.com&quot;;
+    public_baseurl = &quot;https://example.com:8448&quot;;
+
+    enable_registration = false;
+    registration_shared_secret = &quot;xohshaeyui8jic7uutuDogahkee3aehuaf6ei3Xouz4iicie5thie6nohNahceut&quot;;
+    macaroon_secret_key = &quot;xoo8eder9seivukaiPh1cheikohquuw8Yooreid0The4aifahth3Ou0aiShaiz4l&quot;;
+
+    tls_certificate_path = &quot;/var/lib/acme/example.com/fullchain.pem&quot;;
+    tls_certificate_path = &quot;/var/lib/acme/example.com/fullchain.pem&quot;;
+
+    listeners = [ {
+      port = 8448;
+      bind_address = &quot;&quot;;
+      type = &quot;http&quot;;
+      tls = true;
+      resources = [ {
+        names = [ &quot;client&quot; ];
+        compress = true;
+      } {
+        names = [ &quot;federation&quot; ];
+        compress = false;
+      } ];
+    } ];
+
+  };
+}
+</programlisting>
+        <para>
+          After:
+        </para>
+        <programlisting language="bash">
+{
+  services.matrix-synapse = {
+    enable = true;
+
+    # this attribute set holds all values that go into your homeserver.yaml configuration
+    # See https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml for
+    # possible values.
+    settings = {
+      server_name = &quot;example.com&quot;;
+      public_baseurl = &quot;https://example.com:8448&quot;;
+
+      enable_registration = false;
+      # pass `registration_shared_secret` and `macaroon_secret_key` via `extraConfigFiles` instead
+
+      tls_certificate_path = &quot;/var/lib/acme/example.com/fullchain.pem&quot;;
+      tls_certificate_path = &quot;/var/lib/acme/example.com/fullchain.pem&quot;;
+
+      listeners = [ {
+        port = 8448;
+        bind_addresses = [
+          &quot;::&quot;
+          &quot;0.0.0.0&quot;
+        ];
+        type = &quot;http&quot;;
+        tls = true;
+        resources = [ {
+          names = [ &quot;client&quot; ];
+          compress = true;
+        } {
+          names = [ &quot;federation&quot; ];
+          compress = false;
+        } ];
+      } ];
+    };
+
+    extraConfigFiles = [
+      &quot;/run/keys/matrix-synapse/secrets.yaml&quot;
+    ];
+  };
+}
+</programlisting>
+        <para>
+          The secrets in your original config should be migrated into a
+          YAML file that is included via
+          <literal>extraConfigFiles</literal>. The filename must be
+          quoted to prevent nix from copying it to the (world readable)
+          store.
+        </para>
+        <para>
+          Additionally a few option defaults have been synced up with
+          upstream default values, for example the
+          <literal>max_upload_size</literal> grew from
+          <literal>10M</literal> to <literal>50M</literal>. For the same
+          reason, the default <literal>media_store_path</literal> was
+          changed from <literal>${dataDir}/media</literal> to
+          <literal>${dataDir}/media_store</literal> if
+          <literal>system.stateVersion</literal> is at least
+          <literal>22.05</literal>. Files will need to be manually moved
+          to the new location if the <literal>stateVersion</literal> is
+          updated.
+        </para>
+        <para>
+          As of Synapse 1.58.0, the old groups/communities feature has
+          been disabled by default. It will be completely removed with
+          Synapse 1.61.0.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Keycloak package (<literal>pkgs.keycloak</literal>) has
+          been switched from the Wildfly version, which will soon be
+          deprecated, to the Quarkus based version. The Keycloak service
+          (<literal>services.keycloak</literal>) has been updated to
+          accommodate the change and now differs from the previous
+          version in a few ways:
+        </para>
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>services.keycloak.extraConfig</literal> has been
+              removed in favor of the new
+              <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>
+              option. The available options correspond directly to
+              parameters in <literal>conf/keycloak.conf</literal>. Some
+              of the most important parameters are documented as
+              suboptions, the rest can be found in the
+              <link xlink:href="https://www.keycloak.org/server/all-config">All
+              configuration section of the Keycloak Server Installation
+              and Configuration Guide</link>. While the new
+              configuration is much simpler and cleaner than the old
+              JBoss CLI one, this unfortunately mean that there’s no
+              straightforward way to convert an old configuration to the
+              new format and some settings may not even be available
+              anymore.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>services.keycloak.frontendUrl</literal> was
+              removed and the frontend URL is now configured through the
+              <literal>hostname</literal> family of settings in
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>
+              instead. See the
+              <link xlink:href="https://www.keycloak.org/server/hostname">Hostname
+              section of the Keycloak Server Installation and
+              Configuration Guide</link> for more details. Additionally,
+              <literal>/auth</literal> was removed from the default
+              context path and needs to be added back in
+              <link linkend="opt-services.keycloak.settings.http-relative-path"><literal>services.keycloak.settings.http-relative-path</literal></link>
+              if you want to keep compatibility with your current
+              clients.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>services.keycloak.bindAddress</literal>,
+              <literal>services.keycloak.forceBackendUrlToFrontendUrl</literal>,
+              <literal>services.keycloak.httpPort</literal> and
+              <literal>services.keycloak.httpsPort</literal> have been
+              removed in favor of their equivalent options in
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>.
+              <literal>httpPort</literal> and
+              <literal>httpsPort</literal> have additionally had their
+              types changed from <literal>str</literal> to
+              <literal>port</literal>.
+            </para>
+            <para>
+              The new names are as follows:
+            </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  <literal>bindAddress</literal>:
+                  <link linkend="opt-services.keycloak.settings.http-host"><literal>services.keycloak.settings.http-host</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>forceBackendUrlToFrontendUrl</literal>:
+                  <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel"><literal>services.keycloak.settings.hostname-strict-backchannel</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>httpPort</literal>:
+                  <link linkend="opt-services.keycloak.settings.http-port"><literal>services.keycloak.settings.http-port</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>httpsPort</literal>:
+                  <link linkend="opt-services.keycloak.settings.https-port"><literal>services.keycloak.settings.https-port</literal></link>
+                </para>
+              </listitem>
+            </itemizedlist>
+          </listitem>
+        </itemizedlist>
+        <para>
+          For example, when using a reverse proxy the migration could
+          look like this:
+        </para>
+        <para>
+          Before:
+        </para>
+        <programlisting language="bash">
+  services.keycloak = {
+    enable = true;
+    httpPort = &quot;8080&quot;;
+    frontendUrl = &quot;https://keycloak.example.com/auth&quot;;
+    database.passwordFile = &quot;/run/keys/db_password&quot;;
+    extraConfig = {
+      &quot;subsystem=undertow&quot;.&quot;server=default-server&quot;.&quot;http-listener=default&quot;.proxy-address-forwarding = true;
+    };
+  };
+</programlisting>
+        <para>
+          After:
+        </para>
+        <programlisting language="bash">
+  services.keycloak = {
+    enable = true;
+    settings = {
+      http-port = 8080;
+      hostname = &quot;keycloak.example.com&quot;;
+      http-relative-path = &quot;/auth&quot;;
+      proxy = &quot;edge&quot;;
+    };
+    database.passwordFile = &quot;/run/keys/db_password&quot;;
+  };
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
           The MoinMoin wiki engine
           (<literal>services.moinmoin</literal>) has been removed,
           because Python 2 is being retired from nixpkgs.
@@ -303,6 +1179,25 @@
       </listitem>
       <listitem>
         <para>
+          Services in the <literal>hadoop</literal> module previously
+          set <literal>openFirewall</literal> to true by default. This
+          has now been changed to false. Node definitions for multi-node
+          clusters would need <literal>openFirewall = true;</literal> to
+          be added to to hadoop services when upgrading from NixOS
+          21.11.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.hadoop.yarn.nodemanager</literal> now uses
+          cgroup-based CPU limit enforcement by default. Additionally,
+          the option <literal>useCGroups</literal> was added to
+          nodemanagers as an easy way to switch back to the old
+          behavior.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>wafHook</literal> hook now honors
           <literal>NIX_BUILD_CORES</literal> when
           <literal>enableParallelBuilding</literal> is not set
@@ -348,6 +1243,23 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.gnome.experimental-features.realtime-scheduling</literal>
+          option has been removed, as GNOME Shell now
+          <link xlink:href="https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2060">uses
+          rtkit</link>. Use
+          <literal>security.rtkit.enable = true;</literal> instead. As
+          before, you will need to have it enabled using GSettings.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.telepathy</literal> will no longer be
+          enabled by default for GNOME desktops, one should enable it in
+          their configs if using Empathy or Polari.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           If you previously used
           <literal>/etc/docker/daemon.json</literal>, you need to
           incorporate the changes into the new option
@@ -356,6 +1268,15 @@
       </listitem>
       <listitem>
         <para>
+          Ntopng (<literal>services.ntopng</literal>) is updated to
+          5.2.1 and uses a separate Redis instance if
+          <literal>system.stateVersion</literal> is at least
+          <literal>22.05</literal>. Existing setups shouldn’t be
+          affected.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The backward compatibility in
           <literal>services.wordpress</literal> to configure sites with
           the old interface has been removed. Please use
@@ -378,6 +1299,21 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.miniflux.adminCredentialFiles</literal> is
+          now required, instead of defaulting to
+          <literal>admin</literal> and <literal>password</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>taskserver</literal> module no longer implicitly
+          opens ports in the firewall configuration. This is now
+          controlled through the option
+          <literal>services.taskserver.openFirewall</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>autorestic</literal> package has been upgraded
           from 1.3.0 to 1.5.0 which introduces breaking changes in
           config file, check
@@ -387,6 +1323,16 @@
       </listitem>
       <listitem>
         <para>
+          <literal>teleport</literal> has been upgraded to major version
+          9. Please see upstream
+          <link xlink:href="https://goteleport.com/docs/setup/operations/upgrading/">upgrade
+          instructions</link> and
+          <link xlink:href="https://goteleport.com/docs/changelog/#900">release
+          notes</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           For <literal>pkgs.python3.pkgs.ipython</literal>, its direct
           dependency
           <literal>pkgs.python3.pkgs.matplotlib-inline</literal> (which
@@ -459,11 +1405,80 @@
       </listitem>
       <listitem>
         <para>
+          <literal>pkgs._7zz</literal> is now correctly licensed as
+          LGPL3+ and BSD3 with optional unfree unRAR licensed code
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>vim.customize</literal> function produced by
+          <literal>vimUtils.makeCustomizable</literal> now has a
+          slightly different interface:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              The wrapper now includes everything in the given Vim
+              derivation if <literal>name</literal> is
+              <literal>&quot;vim&quot;</literal> (the default). This
+              makes the <literal>wrapManual</literal> argument obsolete,
+              but this behavior can be overriden by setting the
+              <literal>standalone</literal> argument.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              All the executables present in the given derivation (or,
+              in <literal>standalone</literal> mode, only the
+              <literal>*vim</literal> ones) are wrapped. This makes the
+              <literal>wrapGui</literal> argument obsolete.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              The <literal>vimExecutableName</literal> and
+              <literal>gvimExecutableName</literal> arguments were
+              replaced by a single <literal>executableName</literal>
+              argument in which the shell variable
+              <literal>$exe</literal> can be used to refer to the
+              wrapped executable’s name.
+            </para>
+          </listitem>
+        </itemizedlist>
+        <para>
+          See the comments in
+          <literal>pkgs/applications/editors/vim/plugins/vim-utils.nix</literal>
+          for more details.
+        </para>
+        <para>
+          <literal>vimUtils.vimWithRC</literal> was removed. You should
+          instead use <literal>customize</literal> on a Vim derivation,
+          which now accepts <literal>vimrcFile</literal> and
+          <literal>gvimrcFile</literal> arguments.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>tilp2</literal> was removed together with its module
         </para>
       </listitem>
       <listitem>
         <para>
+          The F-PROT antivirus (<literal>fprot</literal> package) and
+          its service module were removed because it reached
+          <link xlink:href="https://kb.cyren.com/av-support/index.php?/Knowledgebase/Article/View/434/0/end-of-sale--end-of-life-for-f-prot-and-csam">end-of-life</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>bird1</literal> and its modules
+          <literal>services.bird</literal> as well as
+          <literal>services.bird6</literal> have been removed. Upgrade
+          to <literal>services.bird2</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The options
           <literal>networking.interfaces.&lt;name&gt;.ipv4.routes</literal>
           and
@@ -476,6 +1491,16 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>miller</literal> package has been upgraded from
+          5.10.3 to
+          <link xlink:href="https://github.com/johnkerl/miller/releases/tag/v6.2.0">6.2.0</link>.
+          See
+          <link xlink:href="https://miller.readthedocs.io/en/latest/new-in-miller-6">What’s
+          new in Miller 6</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           MultiMC has been replaced with the fork PolyMC due to upstream
           developers being hostile to 3rd party package maintainers.
           PolyMC removes all MultiMC branding and is aimed at providing
@@ -492,6 +1517,42 @@
       </listitem>
       <listitem>
         <para>
+          <literal>systemd-nspawn@.service</literal> settings have been
+          reverted to the default systemd behaviour. User namespaces are
+          now activated by default. If you want to keep running nspawn
+          containers without user namespaces you need to set
+          <literal>systemd.nspawn.&lt;name&gt;.execConfig.PrivateUsers = false</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>systemd-shutdown</literal> is now properly linked on
+          shutdown to unmount all filesystems and device mapper devices
+          cleanly. This can be disabled using
+          <literal>systemd.shutdownRamfs.enable</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Tor SOCKS proxy is now actually disabled if
+          <literal>services.tor.client.enable</literal> is set to
+          <literal>false</literal> (the default). If you are using this
+          functionality but didn’t change the setting or set it to
+          <literal>false</literal>, you now need to set it to
+          <literal>true</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.github-runner</literal> has been hardened.
+          Notably address families and system calls have been
+          restricted, which may adversely affect some kinds of testing,
+          e.g. using <literal>AF_BLUETOOTH</literal> to test bluetooth
+          devices.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The terraform 0.12 compatibility has been removed and the
           <literal>terraform.withPlugins</literal> and
           <literal>terraform-providers.mkProvider</literal>
@@ -510,6 +1571,18 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>dendrite</literal> package has been upgraded from
+          0.5.1 to
+          <link xlink:href="https://github.com/matrix-org/dendrite/releases/tag/v0.6.5">0.6.5</link>.
+          Instances configured with split sqlite databases, which has
+          been the default in NixOS, require merging of the federation
+          sender and signing key databases. See upstream
+          <link xlink:href="https://github.com/matrix-org/dendrite/releases/tag/v0.6.0">release
+          notes</link> on version 0.6.0 for details on database changes.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The existing <literal>pkgs.opentelemetry-collector</literal>
           has been moved to
           <literal>pkgs.opentelemetry-collector-contrib</literal> to
@@ -524,6 +1597,33 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.zookeeper</literal> has a new option
+          <literal>jre</literal> for specifying the JRE to start
+          zookeeper with. It defaults to the JRE that
+          <literal>pkgs.zookeeper</literal> was wrapped with, instead of
+          <literal>pkgs.jre</literal>. This changes the JRE to
+          <literal>pkgs.jdk11_headless</literal> by default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.pgadmin</literal> now refers to
+          <literal>pkgs.pgadmin4</literal>. <literal>pgadmin3</literal>
+          has been removed.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.minetestclient_4</literal> and
+          <literal>pkgs.minetestserver_4</literal> have been removed, as
+          the last 4.x release was in 2018.
+          <literal>pkgs.minetestclient</literal> (equivalent to
+          <literal>pkgs.minetest</literal> ) and
+          <literal>pkgs.minetestserver</literal> can be used instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.noto-fonts-cjk</literal> is now deprecated in
           favor of <literal>pkgs.noto-fonts-cjk-sans</literal> and
           <literal>pkgs.noto-fonts-cjk-serif</literal> because they each
@@ -536,6 +1636,58 @@
       </listitem>
       <listitem>
         <para>
+          <literal>pkgs.epgstation</literal> has been upgraded from v1
+          to v2, resulting in incompatible changes in the database
+          scheme and configuration format.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Some top-level settings under
+          <link linkend="opt-services.epgstation.enable">services.epgstation</link>
+          is now deprecated because it was redudant due to the same
+          options being present in
+          <link linkend="opt-services.epgstation.settings">services.epgstation.settings</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option <literal>services.epgstation.basicAuth</literal>
+          was removed because basic authentication support was dropped
+          by upstream.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option
+          <link linkend="opt-services.epgstation.database.passwordFile">services.epgstation.database.passwordFile</link>
+          no longer has a default value. Make sure to set this option
+          explicitly before upgrading. Change the database password if
+          necessary.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The
+          <link linkend="opt-services.epgstation.settings">services.epgstation.settings</link>
+          option now expects options for <literal>config.yml</literal>
+          in EPGStation v2.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Existing data for the
+          <link linkend="opt-services.epgstation.enable">services.epgstation</link>
+          module would have to be backed up prior to the upgrade. To
+          back up exising data to
+          <literal>/tmp/epgstation.bak</literal>, run
+          <literal>sudo -u epgstation epgstation run backup /tmp/epgstation.bak</literal>.
+          To import that data after to the upgrade, run
+          <literal>sudo -u epgstation epgstation run v1migrate /tmp/epgstation.bak</literal>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>switch-to-configuration</literal> (the script that is
           run when running <literal>nixos-rebuild switch</literal> for
           example) has been reworked
@@ -550,6 +1702,15 @@
               honors <literal>restartIfChanged</literal> and
               <literal>reloadIfChanged</literal> of the units.
             </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  Preferring to reload instead of restarting can still
+                  be achieved using
+                  <literal>/run/nixos/activation-reload-list</literal>.
+                </para>
+              </listitem>
+            </itemizedlist>
           </listitem>
           <listitem>
             <para>
@@ -611,12 +1772,153 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>vpnc</literal> package has been changed to use
+          GnuTLS instead of OpenSSL by default for licensing reasons.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The default version of <literal>nextcloud</literal> is
+          <emphasis role="strong">nextcloud24</emphasis>. Please note
+          that it’s <emphasis role="strong">not</emphasis> possible to
+          upgrade <literal>nextcloud</literal> across multiple major
+          versions! This means it’s e.g. not possible to upgrade from
+          <literal>nextcloud22</literal> to
+          <literal>nextcloud24</literal> in a single deploy and most
+          <literal>21.11</literal> users will have to upgrade to
+          <literal>nextcloud23</literal> first.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.vimPlugins.onedark-nvim</literal> now refers to
           <link xlink:href="https://github.com/navarasu/onedark.nvim">navarasu/onedark.nvim</link>
           (formerly refers to
           <link xlink:href="https://github.com/olimorris/onedarkpro.nvim">olimorris/onedarkpro.nvim</link>).
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>services.pipewire.enable</literal> will default to
+          enabling the WirePlumber session manager instead of
+          pipewire-media-session. pipewire-media-session is deprecated
+          by upstream and not recommended, but can still be manually
+          enabled by setting
+          <literal>services.pipewire.media-session.enable</literal> to
+          <literal>true</literal> and
+          <literal>services.pipewire.wireplumber.enable</literal> to
+          <literal>false</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.makeDesktopItem</literal> has been refactored to
+          provide a more idiomatic API. Specifically:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              All valid options as of FDO Desktop Entry specification
+              version 1.4 can now be passed in as explicit arguments
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>exec</literal> can now be null, for entries that
+              are not of type Application
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>mimeType</literal> argument is renamed to
+              <literal>mimeTypes</literal> for consistency
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>mimeTypes</literal>,
+              <literal>categories</literal>,
+              <literal>implements</literal>,
+              <literal>keywords</literal>, <literal>onlyShowIn</literal>
+              and <literal>notShowIn</literal> take lists of strings
+              instead of one string with semicolon separators
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>extraDesktopEntries</literal> renamed to
+              <literal>extraConfig</literal> for consistency
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Actions should now be provided as an attrset
+              <literal>actions</literal>, the <literal>Actions</literal>
+              line will be autogenerated.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>extraEntries</literal> is removed.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Additional validation is added both at eval time and at
+              build time.
+            </para>
+          </listitem>
+        </itemizedlist>
+        <para>
+          See the <literal>vscode</literal> package for a more detailed
+          example.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Existing <literal>resholve*</literal> functions have been
+          renamed and nested under <literal>pkgs.resholve</literal>.
+          Update uses to:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              <literal>resholvePackage</literal> -&gt;
+              <literal>resholve.mkDerivation</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>resholveScript</literal> -&gt;
+              <literal>resholve.writeScript</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>resholveScriptBin</literal> -&gt;
+              <literal>resholve.writeScriptBin</literal>
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.cosmopolitan</literal> no longer provides the
+          <literal>cosmoc</literal> command. It has been moved to
+          <literal>pkgs.cosmoc</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>pkgs.graalvmXX-ce</literal> packages no longer
+          provide support for Python/Ruby/WASM, instead focusing only in
+          Java and Native Image Support. If you need to add support
+          back, please see the
+          <literal>pkgs.graalvmCEPackages.mkGraal</literal> function to
+          create your own customized version of GraalVM with support for
+          what you need.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.05-notable-changes">
@@ -681,7 +1983,8 @@
               Legacy options have been mapped to the corresponding
               options under under
               <link xlink:href="options.html#opt-nix.settings">nix.settings</link>
-              but may be deprecated in the future.
+              and will be deprecated when NixOS 21.11 reaches end of
+              life.
             </para>
           </listitem>
           <listitem>
@@ -694,6 +1997,43 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://kops.sigs.k8s.io"><literal>kops</literal></link>
+          defaults to 1.23.2, which will enable
+          <link xlink:href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html">Instance
+          Metadata Service Version 2</link> and require tokens on new
+          clusters with Kubernetes &gt;= 1.22. This will increase
+          security by default, but may break some types of workloads.
+          The default behaviour for
+          <literal>spec.kubeDNS.nodeLocalDNS.forwardToKubeDNS</literal>
+          has changed from <literal>true</literal> to
+          <literal>false</literal>. Cilium now has
+          <literal>disable-cnp-status-updates: true</literal> by
+          default. Set this to false if you rely on the
+          CiliumNetworkPolicy status fields. Support for Kubernetes
+          1.17, the Lyft CNI, Weave CNI on Kubernetes &gt;= 1.23, CentOS
+          7 and 8, Debian 9, RHEL 7, and Ubuntu 16.05 (Xenial) has been
+          removed. See the
+          <link xlink:href="https://kops.sigs.k8s.io/releases/1.22-notes/">1.22
+          release notes</link> and
+          <link xlink:href="https://kops.sigs.k8s.io/releases/1.23-notes/">1.23
+          release notes</link> for more details, including other
+          significant changes.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Mattermost has been upgraded to extended support version 6.3
+          as the previously packaged extended support version 5.37 is
+          <link xlink:href="https://docs.mattermost.com/upgrade/extended-support-release.html">reaching
+          end of life</link>. Migration may take some time, see the
+          <link xlink:href="https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release">changelog</link>
+          and
+          <link xlink:href="https://docs.mattermost.com/upgrade/important-upgrade-notes.html">important
+          upgrade notes</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The
           <literal>writers.writePyPy2</literal>/<literal>writers.writePyPy3</literal>
           and corresponding
@@ -704,6 +2044,94 @@
       </listitem>
       <listitem>
         <para>
+          Some improvements have been made to the
+          <literal>hadoop</literal> module:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              A <literal>gatewayRole</literal> option has been added,
+              for deploying hadoop cluster configuration files to a node
+              that does not have any active services
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Support for older versions of hadoop have been added to
+              the module
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Overriding and extending site XML files has been made
+              easier
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          The auto-upgrade service now accepts persistent (default:
+          true) parameter. By default auto-upgrade will now run
+          immediately if it would have been triggered at least once
+          during the time when the timer was inactive.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Mastodon now uses <literal>services.redis.servers</literal> to
+          start a new redis server, instead of using a global redis
+          server. This improves compatibility with other services that
+          use redis.
+        </para>
+        <para>
+          Note that this will recreate the redis database, although
+          according to the
+          <link xlink:href="https://docs.joinmastodon.org/admin/backups/">Mastodon
+          docs</link>, this is almost harmless:
+        </para>
+        <blockquote>
+          <para>
+            Losing the Redis database is almost harmless: The only
+            irrecoverable data will be the contents of the Sidekiq
+            queues and scheduled retries of previously failed jobs. The
+            home and list feeds are stored in Redis, but can be
+            regenerated with tootctl.
+          </para>
+        </blockquote>
+        <para>
+          If you do want to save the redis database, you can use the
+          following commands:
+        </para>
+        <programlisting language="bash">
+redis-cli save
+cp /var/lib/redis/dump.rdb &quot;/var/lib/redis-mastodon/dump.rdb&quot;
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
+          Peertube now uses services.redis.servers to start a new redis
+          server, instead of using a global redis server. This improves
+          compatibility with other services that use redis.
+        </para>
+        <para>
+          Redis database is used for storage only cache and job queue.
+          More information can be found here -
+          <link xlink:href="https://docs.joinpeertube.org/contribute-architecture">Peertube
+          architecture</link>.
+        </para>
+        <para>
+          If you do want to save the redis database, you can use the
+          following commands before upgrade OS:
+        </para>
+        <programlisting language="bash">
+redis-cli save
+sudo mkdir /var/lib/redis-peertube
+sudo cp /var/lib/redis/dump.rdb /var/lib/redis-peertube/dump.rdb
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
           If you are using Wayland you can choose to use the Ozone
           Wayland support in Chrome and several Electron apps by setting
           the environment variable <literal>NIXOS_OZONE_WL=1</literal>
@@ -717,6 +2145,35 @@
       </listitem>
       <listitem>
         <para>
+          A new option group
+          <literal>systemd.network.wait-online</literal> was added, with
+          options to configure
+          <literal>systemd-networkd-wait-online.service</literal>:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              <literal>anyInterface</literal> allows specifying that the
+              network should be considered online when <emphasis>at
+              least one</emphasis> interface is online (useful on
+              laptops)
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>timeout</literal> defines how long to wait for
+              the network to come online
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>extraArgs</literal> for everything else
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>influxdb2</literal> package was split into
           <literal>influxdb2-server</literal> and
           <literal>influxdb2-cli</literal>, matching the split that took
@@ -727,6 +2184,29 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>unifi</literal> package was switched from
+          <literal>unifi6</literal> to <literal>unifi7</literal>. Direct
+          downgrades from Unifi 7 to Unifi 6 are not possible and
+          require restoring from a backup made by Unifi 6.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>programs.zsh.autosuggestions.strategy</literal> now
+          takes a list of strings instead of a string.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>asterisk</literal> and
+          <literal>asterisk-stable</literal> packages were switched from
+          <literal>asterisk_18</literal> to the newly-packaged
+          <literal>asterisk_19</literal>. Asterisk 13 and 17 have been
+          removed as they have reached their end of life.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>services.unifi.openPorts</literal> option default
           value of <literal>true</literal> is now deprecated and will be
           changed to <literal>false</literal> in 22.11. Configurations
@@ -735,6 +2215,15 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>services.unifi-video.openPorts</literal> option
+          default value of <literal>true</literal> is now deprecated and
+          will be changed to <literal>false</literal> in 22.11.
+          Configurations using this default will print a warning when
+          rebuilt.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>security.acme</literal> certificates will now
           correctly check for CA revokation before reaching their
           minimum age.
@@ -774,9 +2263,11 @@
         <para>
           <link linkend="opt-programs.ssh.knownHosts">programs.ssh.knownHosts</link>
           has gained an <literal>extraHostNames</literal> option to
-          replace <literal>hostNames</literal>.
-          <literal>hostNames</literal> is deprecated, but still
-          available for now.
+          augment <literal>hostNames</literal>. It is now possible to
+          use the attribute name of a <literal>knownHosts</literal>
+          entry as the primary host name and specify secondary host
+          names using <literal>extraHostNames</literal> without having
+          to duplicate the primary host name.
         </para>
       </listitem>
       <listitem>
@@ -789,6 +2280,29 @@
       </listitem>
       <listitem>
         <para>
+          The option
+          <link linkend="opt-services.xserver.desktopManager.runXdgAutostartIfNone">services.xserver.desktopManager.runXdgAutostartIfNone</link>
+          was added in order to automatically run XDG autostart files
+          for sessions without a desktop manager. This replaces helpers
+          like the <literal>dex</literal> package.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          When setting
+          <link linkend="opt-i18n.inputMethod.enabled">i18n.inputMethod.enabled</link>
+          to <literal>fcitx5</literal>, it no longer creates
+          corresponding systemd user services. It now relies on XDG
+          autostart files to start and work properly in your desktop
+          sessions. If you are using only a window manager without a
+          desktop manager, you need to enable
+          <literal>services.xserver.desktopManager.runXdgAutostartIfNone</literal>
+          or using the <literal>dex</literal> package to make
+          <literal>fcitx5</literal> work.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The option <literal>services.duplicati.dataDir</literal> has
           been added to allow changing the location of duplicati’s
           files.
@@ -796,10 +2310,10 @@
       </listitem>
       <listitem>
         <para>
-          A new option
-          <literal>boot.initrd.extraModprobeConfig</literal> has been
-          added which can be used to configure kernel modules that are
-          loaded in the initrd.
+          The options <literal>boot.extraModprobeConfig</literal> and
+          <literal>boot.blacklistedKernelModules</literal> now also take
+          effect in the initrd by copying the file
+          <literal>/etc/modprobe.d/nixos.conf</literal> into the initrd.
         </para>
       </listitem>
       <listitem>
@@ -811,6 +2325,105 @@
       </listitem>
       <listitem>
         <para>
+          ORY Kratos was updated to version 0.9.0-alpha.3, which
+          introduces some breaking changes:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              All endpoints at the Admin API are now exposed at
+              <literal>/admin/</literal>. For example, endpoint
+              <literal>https://kratos:4434/identities</literal> is now
+              exposed at
+              <literal>https://kratos:4434/admin/identities</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Configuration key
+              <literal>selfservice.whitelisted_return_urls</literal> has
+              been renamed to <literal>allowed_return_urls</literal>
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              The <literal>password_identifier</literal> form field of
+              the password login strategy has been renamed to
+              <literal>identifier</literal> to make compatibility with
+              passwordless flows possible.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Instead of having a global
+              <literal>default_schema_url</literal> which developers
+              used to update their schema, you now need to define the
+              <literal>default_schema_id</literal> which must reference
+              schema ID in your config.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Calling <literal>/self-service/recovery</literal> without
+              flow ID or with an invalid flow ID while authenticated
+              will now respond with an error instead of redirecting to
+              the default page.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              If you are relying on the SQLite images, update your
+              Docker Pull commands as follows:
+            </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  <literal>docker pull oryd/kratos:{version}</literal>
+                </para>
+              </listitem>
+            </itemizedlist>
+          </listitem>
+          <listitem>
+            <para>
+              Additionally, all passwords now have to be at least 8
+              characters long.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              For more details, see:
+            </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  <link xlink:href="https://github.com/ory/kratos/releases/tag/v0.8.1-alpha.1">Release
+                  Notes for v0.8.1-alpha-1</link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <link xlink:href="https://github.com/ory/kratos/releases/tag/v0.8.2-alpha.1">Release
+                  Notes for v0.8.2-alpha-1</link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <link xlink:href="https://github.com/ory/kratos/releases/tag/v0.9.0-alpha.1">Release
+                  Notes for v0.9.0-alpha-1</link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <link xlink:href="https://github.com/ory/kratos/releases/tag/v0.9.0-alpha.3">Release
+                  Notes for v0.9.0-alpha-3</link>
+                </para>
+              </listitem>
+            </itemizedlist>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
           <literal>fetchFromSourcehut</literal> now allows fetching
           repositories recursively using <literal>fetchgit</literal> or
           <literal>fetchhg</literal> if the argument
@@ -820,6 +2433,23 @@
       </listitem>
       <listitem>
         <para>
+          A module for declarative configuration of openconnect VPN
+          profiles was added under
+          <literal>networking.openconnect</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>element-desktop</literal> package now has an
+          <literal>useKeytar</literal> option (defaults to
+          <literal>true</literal>), which allows disabling
+          <literal>keytar</literal> and in turn
+          <literal>libsecret</literal> usage (which binds to native
+          credential managers / keychain libraries).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The option <literal>services.thelounge.plugins</literal> has
           been added to allow installing plugins for The Lounge. Plugins
           can be found in
@@ -829,12 +2459,29 @@
       </listitem>
       <listitem>
         <para>
+          The option
+          <literal>services.xserver.videoDriver = [ &quot;nvidia&quot; ];</literal>
+          will now also install
+          <link xlink:href="https://github.com/elFarto/nvidia-vaapi-driver">nvidia
+          VA-API drivers</link> by default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>firmwareLinuxNonfree</literal> package has been
           renamed to <literal>linux-firmware</literal>.
         </para>
       </listitem>
       <listitem>
         <para>
+          It is now possible to specify wordlists to include as handy to
+          access environment variables using the
+          <literal>config.environment.wordlist</literal> configuration
+          options.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>services.mbpfan</literal> module was converted to
           a
           <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
@@ -851,11 +2498,8 @@
       </listitem>
       <listitem>
         <para>
-          A new module was added for the
-          <link xlink:href="https://starship.rs/">Starship</link> shell
-          prompt, providing the options
-          <literal>programs.starship.enable</literal> and
-          <literal>programs.starship.settings</literal>.
+          The <link xlink:href="https://dino.im">Dino</link> XMPP client
+          was updated to 0.3, adding support for audio and video calls.
         </para>
       </listitem>
       <listitem>
@@ -867,6 +2511,47 @@
       </listitem>
       <listitem>
         <para>
+          <link linkend="opt-services.logrotate.enable">services.logrotate.enable</link>
+          now defaults to true if any rotate path has been defined, and
+          some paths have been added by default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The logrotate module also has been updated to freeform syntax:
+          <link linkend="opt-services.logrotate.paths">services.logrotate.paths</link>
+          and
+          <link linkend="opt-services.logrotate.extraConfig">services.logrotate.extraConfig</link>
+          will work, but issue deprecation warnings and
+          <link linkend="opt-services.logrotate.settings">services.logrotate.settings</link>
+          should now be used instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>security.pam.ussh</literal> has been added, which
+          allows authorizing PAM sessions based on SSH
+          <emphasis>certificates</emphasis> held within an SSH agent,
+          using
+          <link xlink:href="https://github.com/uber/pam-ussh">pam-ussh</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>vscode-extensions.ionide.ionide-fsharp</literal>
+          package has been updated to 6.0.0 and now requires .NET 6.0.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>phpPackages.box</literal> package has been
+          updated from 2.7.5 to 3.16.0. See the
+          <link xlink:href="https://github.com/box-project/box/blob/master/UPGRADE.md#from-27-to-30">upgrade
+          guide</link> for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>zrepl</literal> package has been updated from
           0.4.0 to 0.5:
         </para>
@@ -892,6 +2577,24 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>polybar</literal> package has been updated from
+          3.5.7 to 3.6.2. See
+          <link xlink:href="https://github.com/polybar/polybar/releases/tag/3.6.0">the
+          changelog</link> for more details.
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              Breaking changes include changes to escaping rules in
+              configuration values, changes in behavior when
+              encountering invalid tag names, and changes to
+              inter-process-messaging (IPC).
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
           Renamed option
           <literal>services.openssh.challengeResponseAuthentication</literal>
           to
@@ -903,8 +2606,24 @@
       </listitem>
       <listitem>
         <para>
+          <literal>services.autorandr</literal> now allows for adding
+          hooks and profiles declaratively.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>pomerium-cli</literal> command has been moved out
+          of the <literal>pomerium</literal> package into the
+          <literal>pomerium-cli</literal> package, following upstream’s
+          repository split. If you are using the
+          <literal>pomerium-cli</literal> command, you should now
+          install the <literal>pomerium-cli</literal> package.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The option
-          <link linkend="opt-services.networking.networkmanager.enableFccUnlock">services.networking.networkmanager.enableFccUnlock</link>
+          <link linkend="opt-networking.networkmanager.enableFccUnlock">services.networking.networkmanager.enableFccUnlock</link>
           was added to support FCC unlock procedures. Since release
           1.18.4, the ModemManager daemon no longer automatically
           performs the FCC unlock procedure by default. See
@@ -921,6 +2640,183 @@
           <literal>tmux</literal>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          The polkit service, available at
+          <literal>security.polkit.enable</literal>, is now disabled by
+          default. It will automatically be enabled through services and
+          desktop environments as needed.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>mercury</literal> was updated to 22.01.1, which has
+          some breaking changes
+          (<link xlink:href="https://dl.mercurylang.org/release/release-notes-22.01.html">Mercury
+          22.01 news</link>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          xfsprogs was update to version 5.15, which enables inobtcount
+          and bigtime by default on filesystem creation. Support for
+          these features was added in kernel 5.10 and deemed stable in
+          kernel 5.15. If you want to be able to mount XFS filesystems
+          created with this release of xfsprogs on kernel releases older
+          than 5.10, you need to format them with
+          <literal>mkfs.xfs -m bigtime=0 -m inobtcount=0</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.xserver.desktopManager.xfce</literal> now
+          includes Xfce’s screen locker,
+          <literal>xfce4-screensaver</literal> that is enabled by
+          default. You can disable it by setting
+          <literal>false</literal> to
+          <link linkend="opt-services.xserver.desktopManager.xfce.enableScreensaver">services.xserver.desktopManager.xfce.enableScreensaver</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>hadoop</literal> package has added support for
+          <literal>aarch64-linux</literal> and
+          <literal>aarch64-darwin</literal> as of 3.3.1
+          (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/158613">#158613</link>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>R</literal> package now builds again on
+          <literal>aarch64-darwin</literal>
+          (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/158992">#158992</link>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>nss</literal> package was split into
+          <literal>nss_esr</literal> and <literal>nss_latest</literal>,
+          with <literal>nss</literal> being an alias for
+          <literal>nss_esr</literal>. This was done to ease maintenance
+          of <literal>nss</literal> and dependent high-profile packages
+          like <literal>firefox</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The default <literal>scribus</literal> version is now 1.5,
+          while version 1.4 is still available as
+          <literal>scribus_1_4</literal>
+          (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/172700">#172700</link>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Nextcloud module now supports to create a Mysql database
+          automatically with
+          <literal>services.nextcloud.database.createLocally</literal>
+          enabled.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Nextcloud module now allows setting the value of the
+          <literal>max-age</literal> directive of the
+          <literal>Strict-Transport-Security</literal> HTTP header,
+          which is now controlled by the
+          <literal>services.nextcloud.https</literal> option, rather
+          than <literal>services.nginx.recommendedHttpHeaders</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>spark3</literal> package has been updated from
+          3.1.2 to 3.2.1
+          (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/160075">#160075</link>):
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              Testing has been enabled for
+              <literal>aarch64-linux</literal> in addition to
+              <literal>x86_64-linux</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              The <literal>spark3</literal> package is now usable on
+              <literal>aarch64-darwin</literal> as a result of
+              <link xlink:href="https://github.com/NixOS/nixpkgs/pull/158613">#158613</link>
+              and
+              <link xlink:href="https://github.com/NixOS/nixpkgs/pull/158992">#158992</link>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          The option <literal>services.snapserver.openFirewall</literal>
+          will no longer default to <literal>true</literal> starting
+          with NixOS 22.11. Enable it explicitly if you need to control
+          Snapserver remotely or connect streamig clients from other
+          hosts.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option
+          <link xlink:href="options.html#opt-networking.useDHCP">networking.useDHCP</link>
+          isn’t deprecated anymore. When using
+          <link xlink:href="options.html#opt-networking.useNetworkd"><literal>systemd-networkd</literal></link>,
+          a generic <literal>.network</literal>-unit is added which
+          enables DHCP for each interface matching
+          <literal>en*</literal>, <literal>eth*</literal> or
+          <literal>wl*</literal> with priority 99 (which means that it
+          doesn’t have any effect if such an interface is matched by a
+          <literal>.network-</literal>unit with a lower priority). In
+          case of scripted networking, no behavior was changed.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The new
+          <link xlink:href="https://nixos.org/manual/nixpkgs/stable/#sec-postgresqlTestHook"><literal>postgresqlTestHook</literal></link>
+          runs a PostgreSQL server for the duration of package checks.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>zfs</literal> was updated from 2.1.4 to 2.1.5,
+          enabling it to be used with Linux kernel 5.18.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>stdenv.mkDerivation</literal> now supports a
+          self-referencing <literal>finalAttrs:</literal> parameter
+          containing the final <literal>mkDerivation</literal> arguments
+          including overrides. <literal>drv.overrideAttrs</literal> now
+          supports two parameters
+          <literal>finalAttrs: previousAttrs:</literal>. This allows
+          packaging configuration to be overridden in a consistent
+          manner by providing an alternative to
+          <literal>rec {}</literal> syntax.
+        </para>
+        <para>
+          Additionally, <literal>passthru</literal> can now reference
+          <literal>finalAttrs.finalPackage</literal> containing the
+          final package, including attributes such as the output paths
+          and <literal>overrideAttrs</literal>.
+        </para>
+        <para>
+          New language integrations can be simplified by overriding a
+          <quote>prototype</quote> package containing the
+          language-specific logic. This removes the need for a extra
+          layer of overriding for the <quote>generic builder</quote>
+          arguments, thus removing a usability problem and source of
+          error.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
new file mode 100644
index 0000000000000..be3adc4d3bed9
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -0,0 +1,381 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-22.11">
+  <title>Release 22.11 (“Raccoon”, 2022.11/??)</title>
+  <para>
+    Support is planned until the end of June 2023, handing over to
+    23.05.
+  </para>
+  <section xml:id="sec-release-22.11-highlights">
+    <title>Highlights</title>
+    <para>
+      In addition to numerous new and upgraded packages, this release
+      has the following highlights:
+    </para>
+    <itemizedlist>
+      <listitem>
+        <para>
+          During cross-compilation, tests are now executed if the test
+          suite can be executed by the build platform. This is the case
+          when doing “native” cross-compilation where the build and host
+          platforms are largely the same, but the nixpkgs’ cross
+          compilation infrastructure is used, e.g.
+          <literal>pkgsStatic</literal> and <literal>pkgsLLVM</literal>.
+          Another possibility is that the build platform is a superset
+          of the host platform, e.g. when cross-compiling from
+          <literal>x86_64-unknown-linux</literal> to
+          <literal>i686-unknown-linux</literal>. The predicate gating
+          test suite execution is the newly added
+          <literal>canExecute</literal> predicate: You can e.g. check if
+          <literal>stdenv.buildPlatform</literal> can execute binaries
+          built for <literal>stdenv.hostPlatform</literal> (i.e.
+          produced by <literal>stdenv.cc</literal>) by evaluating
+          <literal>stdenv.buildPlatform.canExecute stdenv.hostPlatform</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>nixpkgs.hostPlatform</literal> and
+          <literal>nixpkgs.buildPlatform</literal> options have been
+          added. These cover and override the
+          <literal>nixpkgs.{system,localSystem,crossSystem}</literal>
+          options.
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              <literal>hostPlatform</literal> is the platform or
+              <quote><literal>system</literal></quote> string of the
+              NixOS system described by the configuration.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>buildPlatform</literal> is the platform that is
+              responsible for building the NixOS configuration. It
+              defaults to the <literal>hostPlatform</literal>, for a
+              non-cross build configuration. To cross compile, set
+              <literal>buildPlatform</literal> to a different value.
+            </para>
+          </listitem>
+        </itemizedlist>
+        <para>
+          The new options convey the same information, but with fewer
+          options, and following the Nixpkgs terminology.
+        </para>
+        <para>
+          The existing options
+          <literal>nixpkgs.{system,localSystem,crossSystem}</literal>
+          have not been formally deprecated, to allow for evaluation of
+          the change and to allow for a transition period so that in
+          time the ecosystem can switch without breaking compatibility
+          with any supported NixOS release.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>nixos-generate-config</literal> now generates
+          configurations that can be built in pure mode. This is
+          achieved by setting the new
+          <literal>nixpkgs.hostPlatform</literal> option.
+        </para>
+        <para>
+          You may have to unset the <literal>system</literal> parameter
+          in <literal>lib.nixosSystem</literal>, or similarly remove
+          definitions of the
+          <literal>nixpkgs.{system,localSystem,crossSystem}</literal>
+          options.
+        </para>
+        <para>
+          Alternatively, you can remove the
+          <literal>hostPlatform</literal> line and use NixOS like you
+          would in NixOS 22.05 and earlier.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PHP now defaults to PHP 8.1, updated from 8.0.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>hardware.nvidia</literal> has a new option
+          <literal>open</literal> that can be used to opt in the
+          opensource version of NVIDIA kernel driver. Note that the
+          driver’s support for GeForce and Workstation GPUs is still
+          alpha quality, see
+          <link xlink:href="https://developer.nvidia.com/blog/nvidia-releases-open-source-gpu-kernel-modules/">NVIDIA
+          Releases Open-Source GPU Kernel Modules</link> for the
+          official announcement.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="sec-release-22.11-new-services">
+    <title>New Services</title>
+    <itemizedlist>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/jollheef/appvm">appvm</link>,
+          Nix based app VMs. Available as
+          <link xlink:href="options.html#opt-virtualisation.appvm.enable">virtualisation.appvm</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://dragonflydb.io/">dragonflydb</link>,
+          a modern replacement for Redis and Memcached. Available as
+          <link linkend="opt-services.dragonflydb.enable">services.dragonflydb</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/leetronics/infnoise">infnoise</link>,
+          a hardware True Random Number Generator dongle. Available as
+          <link xlink:href="options.html#opt-services.infnoise.enable">services.infnoise</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/aiberia/persistent-evdev">persistent-evdev</link>,
+          a daemon to add virtual proxy devices that mirror a physical
+          input device but persist even if the underlying hardware is
+          hot-plugged. Available as
+          <link linkend="opt-services.persistent-evdev.enable">services.persistent-evdev</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://schleuder.org/">schleuder</link>, a
+          mailing list manager with PGP support. Enable using
+          <link linkend="opt-services.schleuder.enable">services.schleuder</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.expressvpn.com">expressvpn</link>,
+          the CLI client for ExpressVPN. Available as
+          <link linkend="opt-services.expressvpn.enable">services.expressvpn</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.grafana.com/oss/tempo/">Grafana
+          Tempo</link>, a distributed tracing store. Available as
+          <link linkend="opt-services.tempo.enable">services.tempo</link>.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="sec-release-22.11-incompatibilities">
+    <title>Backward Incompatibilities</title>
+    <itemizedlist>
+      <listitem>
+        <para>
+          The <literal>isCompatible</literal> predicate checking CPU
+          compatibility is no longer exposed by the platform sets
+          generated using <literal>lib.systems.elaborate</literal>. In
+          most cases you will want to use the new
+          <literal>canExecute</literal> predicate instead which also
+          considers the kernel / syscall interface. It is briefly
+          described in the release’s
+          <link linkend="sec-release-22.11-highlights">highlights
+          section</link>.
+          <literal>lib.systems.parse.isCompatible</literal> still
+          exists, but has changed semantically: Architectures with
+          differing endianness modes are <emphasis>no longer considered
+          compatible</emphasis>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>ngrok</literal> has been upgraded from 2.3.40 to
+          3.0.4. Please see
+          <link xlink:href="https://ngrok.com/docs/guides/upgrade-v2-v3">the
+          upgrade guide</link> and
+          <link xlink:href="https://ngrok.com/docs/ngrok-agent/changelog">changelog</link>.
+          Notably, breaking changes are that the config file format has
+          changed and support for single hypen arguments was dropped.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>i18n.supportedLocales</literal> is now by default
+          only generated with the locales set in
+          <literal>i18n.defaultLocale</literal> and
+          <literal>i18n.extraLocaleSettings</literal>. This got
+          partially copied over from the minimal profile and reduces the
+          final system size by up to 200MB. If you require all locales
+          installed set the option to
+          <literal>[ &quot;all&quot; ]</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>isPowerPC</literal> predicate, found on
+          <literal>platform</literal> attrsets
+          (<literal>hostPlatform</literal>,
+          <literal>buildPlatform</literal>,
+          <literal>targetPlatform</literal>, etc) has been removed in
+          order to reduce confusion. The predicate was was defined such
+          that it matches only the 32-bit big-endian members of the
+          POWER/PowerPC family, despite having a name which would imply
+          a broader set of systems. If you were using this predicate,
+          you can replace <literal>foo.isPowerPC</literal> with
+          <literal>(with foo; isPower &amp;&amp; is32bit &amp;&amp; isBigEndian)</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>bsp-layout</literal> no longer uses the command
+          <literal>cycle</literal> to switch to other window layouts, as
+          it got replaced by the commands <literal>previous</literal>
+          and <literal>next</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Barco ClickShare driver/client package
+          <literal>pkgs.clickshare-csc1</literal> and the option
+          <literal>programs.clickshare-csc1.enable</literal> have been
+          removed, as it requires <literal>qt4</literal>, which reached
+          its end-of-life 2015 and will no longer be supported by
+          nixpkgs.
+          <link xlink:href="https://www.barco.com/de/support/knowledge-base/4380-can-i-use-linux-os-with-clickshare-base-units">According
+          to Barco</link> many of their base unit models can be used
+          with Google Chrome and the Google Cast extension.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PHP 7.4 is no longer supported due to upstream not supporting
+          this version for the entire lifecycle of the 22.11 release.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          riak package removed along with
+          <literal>services.riak</literal> module, due to lack of
+          maintainer to update the package.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.graphite.api</literal> and
+          <literal>services.graphite.beacon</literal> NixOS options, and
+          the <literal>python3.pkgs.graphite_api</literal>,
+          <literal>python3.pkgs.graphite_beacon</literal> and
+          <literal>python3.pkgs.influxgraph</literal> packages, have
+          been removed due to lack of upstream maintenance.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>meta.mainProgram</literal> attribute of packages
+          in <literal>wineWowPackages</literal> now defaults to
+          <literal>&quot;wine64&quot;</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          (Neo)Vim can not be configured with
+          <literal>configure.pathogen</literal> anymore to reduce
+          maintainance burden. Use <literal>configure.packages</literal>
+          instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>k3s</literal> no longer supports docker as runtime
+          due to upstream dropping support.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="sec-release-22.11-notable-changes">
+    <title>Other Notable Changes</title>
+    <itemizedlist>
+      <listitem>
+        <para>
+          The <literal>xplr</literal> package has been updated from
+          0.18.0 to 0.19.0, which brings some breaking changes. See the
+          <link xlink:href="https://github.com/sayanarijit/xplr/releases/tag/v0.19.0">upstream
+          release notes</link> for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          A new module was added for the Saleae Logic device family,
+          providing the options
+          <literal>hardware.saleae-logic.enable</literal> and
+          <literal>hardware.saleae-logic.package</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The Redis module now disables RDB persistence when
+          <literal>services.redis.servers.&lt;name&gt;.save = []</literal>
+          instead of using the Redis default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Matrix Synapse now requires entries in the
+          <literal>state_group_edges</literal> table to be unique, in
+          order to prevent accidentally introducing duplicate
+          information (for example, because a database backup was
+          restored multiple times). If your Synapse database already has
+          duplicate rows in this table, this could fail with an error
+          and require manual remediation.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>dockerTools.buildImage</literal> deprecates the
+          misunderstood <literal>contents</literal> parameter, in favor
+          of <literal>copyToRoot</literal>. Use
+          <literal>copyToRoot = buildEnv { ... };</literal> or similar
+          if you intend to add packages to <literal>/bin</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          memtest86+ was updated from 5.00-coreboot-002 to 6.00-beta2.
+          It is now the upstream version from https://www.memtest.org/,
+          as coreboot’s fork is no longer available.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Add udev rules for the Teensy family of microcontrollers.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>pass-secret-service</literal> package now
+          includes systemd units from upstream, so adding it to the
+          NixOS <literal>services.dbus.packages</literal> option will
+          make it start automatically as a systemd user service when an
+          application tries to talk to the libsecret D-Bus API.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          There is a new module for the <literal>thunar</literal>
+          program (the Xfce file manager), which depends on the
+          <literal>xfconf</literal> dbus service, and also has a dbus
+          service and a systemd unit. The option
+          <literal>services.xserver.desktopManager.xfce.thunarPlugins</literal>
+          has been renamed to
+          <literal>programs.thunar.plugins</literal>, and in a future
+          release it may be removed.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          There is a new module for the <literal>xfconf</literal>
+          program (the Xfce configuration storage system), which has a
+          dbus service.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+</section>
diff --git a/nixos/doc/manual/development/building-nixos.chapter.md b/nixos/doc/manual/installation/building-nixos.chapter.md
index 3310dee98f96b..17da261fbdaa4 100644
--- a/nixos/doc/manual/development/building-nixos.chapter.md
+++ b/nixos/doc/manual/installation/building-nixos.chapter.md
@@ -18,9 +18,12 @@ enforced values with `mkForce`.
 
 ## Practical Instructions {#sec-building-image-instructions}
 
+To build an ISO image for the channel `nixos-unstable`:
+
 ```ShellSession
 $ git clone https://github.com/NixOS/nixpkgs.git
 $ cd nixpkgs/nixos
+$ git switch nixos-unstable
 $ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-minimal.nix default.nix
 ```
 
@@ -30,6 +33,37 @@ To check the content of an ISO image, mount it like so:
 # mount -o loop -t iso9660 ./result/iso/cd.iso /mnt/iso
 ```
 
+## Additional drivers or firmware {#sec-building-image-drivers}
+
+If you need additional (non-distributable) drivers or firmware in the
+installer, you might want to extend these configurations.
+
+For example, to build the GNOME graphical installer ISO, but with support for
+certain WiFi adapters present in some MacBooks, you can create the following
+file at `modules/installer/cd-dvd/installation-cd-graphical-gnome-macbook.nix`:
+
+```nix
+{ config, ... }:
+
+{
+  imports = [ ./installation-cd-graphical-gnome.nix ];
+
+  boot.initrd.kernelModules = [ "wl" ];
+
+  boot.kernelModules = [ "kvm-intel" "wl" ];
+  boot.extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];
+}
+```
+
+Then build it like in the example above:
+
+```ShellSession
+$ git clone https://github.com/NixOS/nixpkgs.git
+$ cd nixpkgs/nixos
+$ export NIXPKGS_ALLOW_UNFREE=1
+$ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-graphical-gnome-macbook.nix default.nix
+```
+
 ## Technical Notes {#sec-building-image-tech-notes}
 
 The config value enforcement is implemented via `mkImageMediaOverride = mkOverride 60;`
diff --git a/nixos/doc/manual/installation/installation.xml b/nixos/doc/manual/installation/installation.xml
index 1d443bbd0ee16..ba07d71d0ca35 100644
--- a/nixos/doc/manual/installation/installation.xml
+++ b/nixos/doc/manual/installation/installation.xml
@@ -14,4 +14,5 @@
  <xi:include href="../from_md/installation/installing.chapter.xml" />
  <xi:include href="../from_md/installation/changing-config.chapter.xml" />
  <xi:include href="../from_md/installation/upgrading.chapter.xml" />
+ <xi:include href="../from_md/installation/building-nixos.chapter.xml" />
 </part>
diff --git a/nixos/doc/manual/installation/installing-from-other-distro.section.md b/nixos/doc/manual/installation/installing-from-other-distro.section.md
index d9060eb89c372..fa8806f791d52 100644
--- a/nixos/doc/manual/installation/installing-from-other-distro.section.md
+++ b/nixos/doc/manual/installation/installing-from-other-distro.section.md
@@ -177,7 +177,7 @@ The first steps to all these are the same:
     was probably single user):
 
     ```ShellSession
-    $ sudo chown -R 0.0 /nix
+    $ sudo chown -R 0:0 /nix
     ```
 
 1.  Set up the `/etc/NIXOS` and `/etc/NIXOS_LUSTRATE` files:
diff --git a/nixos/doc/manual/installation/installing-kexec.section.md b/nixos/doc/manual/installation/installing-kexec.section.md
new file mode 100644
index 0000000000000..286cbbda6a69e
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-kexec.section.md
@@ -0,0 +1,64 @@
+# "Booting" into NixOS via kexec {#sec-booting-via-kexec}
+
+In some cases, your system might already be booted into/preinstalled with
+another Linux distribution, and booting NixOS by attaching an installation
+image is quite a manual process.
+
+This is particularly useful for (cloud) providers where you can't boot a custom
+image, but get some Debian or Ubuntu installation.
+
+In these cases, it might be easier to use `kexec` to "jump into NixOS" from the
+running system, which only assumes `bash` and `kexec` to be installed on the
+machine.
+
+Note that kexec may not work correctly on some hardware, as devices are not
+fully re-initialized in the process. In practice, this however is rarely the
+case.
+
+To build the necessary files from your current version of nixpkgs,
+you can run:
+
+```ShellSession
+nix-build -A kexec.x86_64-linux '<nixpkgs/nixos/release.nix>'
+```
+
+This will create a `result` directory containing the following:
+ - `bzImage` (the Linux kernel)
+ - `initrd` (the initrd file)
+ - `kexec-boot` (a shellscript invoking `kexec`)
+
+These three files are meant to be copied over to the other already running
+Linux Distribution.
+
+Note it's symlinks pointing elsewhere, so `cd` in, and use
+`scp * root@$destination` to copy it over, rather than rsync.
+
+Once you finished copying, execute `kexec-boot` *on the destination*, and after
+some seconds, the machine should be booting into an (ephemeral) NixOS
+installation medium.
+
+In case you want to describe your own system closure to kexec into, instead of
+the default installer image, you can build your own `configuration.nix`:
+
+```nix
+{ modulesPath, ... }: {
+  imports = [
+    (modulesPath + "/installer/netboot/netboot-minimal.nix")
+  ];
+
+  services.openssh.enable = true;
+  users.users.root.openssh.authorizedKeys.keys = [
+    "my-ssh-pubkey"
+  ];
+}
+```
+
+
+```ShellSession
+nix-build '<nixpkgs/nixos>' \
+  --arg configuration ./configuration.nix
+  --attr config.system.build.kexecTree
+```
+
+Make sure your `configuration.nix` does still import `netboot-minimal.nix` (or
+`netboot-base.nix`).
diff --git a/nixos/doc/manual/installation/installing-pxe.section.md b/nixos/doc/manual/installation/installing-pxe.section.md
index 2016a258251f8..4fbd6525f8c3b 100644
--- a/nixos/doc/manual/installation/installing-pxe.section.md
+++ b/nixos/doc/manual/installation/installing-pxe.section.md
@@ -5,11 +5,11 @@ setup.
 
 These instructions assume that you have an existing PXE or iPXE
 infrastructure and simply want to add the NixOS installer as another
-option. To build the necessary files from a recent version of nixpkgs,
+option. To build the necessary files from your current version of nixpkgs,
 you can run:
 
 ```ShellSession
-nix-build -A netboot.x86_64-linux nixos/release.nix
+nix-build -A netboot.x86_64-linux '<nixpkgs/nixos/release.nix>'
 ```
 
 This will create a `result` directory containing: \* `bzImage` -- the
diff --git a/nixos/doc/manual/installation/installing-usb.section.md b/nixos/doc/manual/installation/installing-usb.section.md
index ae58c08e5237e..d893e22e6381b 100644
--- a/nixos/doc/manual/installation/installing-usb.section.md
+++ b/nixos/doc/manual/installation/installing-usb.section.md
@@ -18,7 +18,7 @@ $ diskutil list
 [..]
 $ diskutil unmountDisk diskN
 Unmount of all volumes on diskN was successful
-$ sudo dd if=nix.iso of=/dev/rdiskN
+$ sudo dd if=nix.iso of=/dev/rdiskN bs=1M
 ```
 
 Using the \'raw\' `rdiskN` device instead of `diskN` completes in
diff --git a/nixos/doc/manual/installation/installing.chapter.md b/nixos/doc/manual/installation/installing.chapter.md
index def4f37fbcaa4..7e830f8e4583b 100644
--- a/nixos/doc/manual/installation/installing.chapter.md
+++ b/nixos/doc/manual/installation/installing.chapter.md
@@ -296,6 +296,11 @@ Use the following commands:
         specify on which disk the GRUB boot loader is to be installed.
         Without it, NixOS cannot boot.
 
+    :   If there are other operating systems running on the machine before
+        installing NixOS, the [](#opt-boot.loader.grub.useOSProber)
+        option can be set to `true` to automatically add them to the grub
+        menu.
+
     UEFI systems
 
     :   You *must* set the option [](#opt-boot.loader.systemd-boot.enable)
@@ -307,11 +312,6 @@ Use the following commands:
         [`boot.loader.systemd-boot`](#opt-boot.loader.systemd-boot.enable)
         as well.
 
-    If there are other operating systems running on the machine before
-    installing NixOS, the [](#opt-boot.loader.grub.useOSProber)
-    option can be set to `true` to automatically add them to the grub
-    menu.
-
     If you need to configure networking for your machine the
     configuration options are described in [](#sec-networking). In
     particular, while wifi is supported on the installation image, it is
@@ -476,6 +476,7 @@ With a partitioned disk.
 ```{=docbook}
 <xi:include href="installing-usb.section.xml" />
 <xi:include href="installing-pxe.section.xml" />
+<xi:include href="installing-kexec.section.xml" />
 <xi:include href="installing-virtualbox-guest.section.xml" />
 <xi:include href="installing-from-other-distro.section.xml" />
 <xi:include href="installing-behind-a-proxy.section.xml" />
diff --git a/nixos/doc/manual/installation/upgrading.chapter.md b/nixos/doc/manual/installation/upgrading.chapter.md
index faeefc4451dc9..2644979bc9db2 100644
--- a/nixos/doc/manual/installation/upgrading.chapter.md
+++ b/nixos/doc/manual/installation/upgrading.chapter.md
@@ -6,7 +6,7 @@ expressions and associated binaries. The NixOS channels are updated
 automatically from NixOS's Git repository after certain tests have
 passed and all packages have been built. These channels are:
 
--   *Stable channels*, such as [`nixos-21.11`](https://nixos.org/channels/nixos-21.11).
+-   *Stable channels*, such as [`nixos-22.05`](https://nixos.org/channels/nixos-22.05).
     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
@@ -19,7 +19,7 @@ passed and all packages have been built. These channels are:
     radical changes between channel updates. It's not recommended for
     production systems.
 
--   *Small channels*, such as [`nixos-21.11-small`](https://nixos.org/channels/nixos-21.11-small)
+-   *Small channels*, such as [`nixos-22.05-small`](https://nixos.org/channels/nixos-22.05-small)
     or [`nixos-unstable-small`](https://nixos.org/channels/nixos-unstable-small).
     These are identical to the stable and unstable channels described above,
     except that they contain fewer binary packages. This means they get updated
@@ -38,8 +38,8 @@ newest supported stable release.
 
 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 21.11 ISO, you will be subscribed to
-the `nixos-21.11` channel. To see which NixOS channel you're subscribed
+instance, if you installed from a 22.05 ISO, you will be subscribed to
+the `nixos-22.05` channel. To see which NixOS channel you're subscribed
 to, run the following as root:
 
 ```ShellSession
@@ -54,16 +54,16 @@ To switch to a different NixOS channel, do
 ```
 
 (Be sure to include the `nixos` parameter at the end.) For instance, to
-use the NixOS 21.11 stable channel:
+use the NixOS 22.05 stable channel:
 
 ```ShellSession
-# nix-channel --add https://nixos.org/channels/nixos-21.11 nixos
+# nix-channel --add https://nixos.org/channels/nixos-22.05 nixos
 ```
 
 If you have a server, you may want to use the "small" channel instead:
 
 ```ShellSession
-# nix-channel --add https://nixos.org/channels/nixos-21.11-small nixos
+# nix-channel --add https://nixos.org/channels/nixos-22.05-small nixos
 ```
 
 And if you want to live on the bleeding edge:
@@ -114,5 +114,5 @@ the new generation contains a different kernel, initrd or kernel
 modules. You can also specify a channel explicitly, e.g.
 
 ```nix
-system.autoUpgrade.channel = https://nixos.org/channels/nixos-21.11;
+system.autoUpgrade.channel = https://nixos.org/channels/nixos-22.05;
 ```
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index ab2a5d83a089f..ea96f49fa9772 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -92,6 +92,10 @@
    </arg>
 
    <arg>
+    <option>--no-flake</option>
+   </arg>
+
+   <arg>
     <option>--override-input</option> <replaceable>input-name</replaceable> <replaceable>flake-uri</replaceable>
    </arg>
 
@@ -544,6 +548,14 @@
       (<replaceable>user@host</replaceable>). You can also set ssh options by
       defining the <envar>NIX_SSHOPTS</envar> environment variable.
      </para>
+
+     <para>
+      Note that <command>nixos-rebuild</command> honors the
+      <literal>nixpkgs.crossSystem</literal> setting of the given configuration
+      but disregards the true architecture of the target host. Hence the
+      <literal>nixpkgs.crossSystem</literal> setting has to match the target
+      platform or else activation will fail.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -594,6 +606,20 @@
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term>
+     <option>--no-flake</option>
+    </term>
+    <listitem>
+     <para>
+      Do not imply <option>--flake</option> if
+      <filename>/etc/nixos/flake.nix</filename> exists. With this
+      option, it is possible to build non-flake NixOS configurations
+      even if the current NixOS systems uses flakes.
+     </para>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
 
   <para>
diff --git a/nixos/doc/manual/man-pages.xml b/nixos/doc/manual/man-pages.xml
index 49acfe7330b6d..58f73521e94fd 100644
--- a/nixos/doc/manual/man-pages.xml
+++ b/nixos/doc/manual/man-pages.xml
@@ -3,10 +3,15 @@
            xmlns:xi="http://www.w3.org/2001/XInclude">
  <title>NixOS Reference Pages</title>
  <info>
-  <author><personname><firstname>Eelco</firstname><surname>Dolstra</surname></personname>
+  <author>
+   <personname><firstname>Eelco</firstname><surname>Dolstra</surname></personname>
    <contrib>Author</contrib>
   </author>
-  <copyright><year>2007-2020</year><holder>Eelco Dolstra</holder>
+  <author>
+   <personname><othername>The Nixpkgs/NixOS contributors</othername></personname>
+   <contrib>Author</contrib>
+  </author>
+  <copyright><year>2007-2022</year><holder>Eelco Dolstra and the Nixpkgs/NixOS contributors</holder>
   </copyright>
  </info>
  <xi:include href="man-configuration.xml" />
diff --git a/nixos/doc/manual/md-to-db.sh b/nixos/doc/manual/md-to-db.sh
index e0274f5619c70..2091f9b31cd2f 100755
--- a/nixos/doc/manual/md-to-db.sh
+++ b/nixos/doc/manual/md-to-db.sh
@@ -1,5 +1,5 @@
 #! /usr/bin/env nix-shell
-#! nix-shell -I nixpkgs=channel:nixpkgs-unstable -i bash -p pandoc
+#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/tarball/21.11 -i bash -p pandoc
 
 # This script is temporarily needed while we transition the manual to
 # CommonMark. It converts the .md files in the regular manual folder
diff --git a/nixos/doc/manual/release-notes/release-notes.xml b/nixos/doc/manual/release-notes/release-notes.xml
index 74ca57850ea56..ee5009faf6f46 100644
--- a/nixos/doc/manual/release-notes/release-notes.xml
+++ b/nixos/doc/manual/release-notes/release-notes.xml
@@ -8,6 +8,8 @@
   This section lists the release notes for each stable version of NixOS and
   current unstable revision.
  </para>
+ <xi:include href="../from_md/release-notes/rl-2211.section.xml" />
+ <xi:include href="../from_md/release-notes/rl-2205.section.xml" />
  <xi:include href="../from_md/release-notes/rl-2111.section.xml" />
  <xi:include href="../from_md/release-notes/rl-2105.section.xml" />
  <xi:include href="../from_md/release-notes/rl-2009.section.xml" />
diff --git a/nixos/doc/manual/release-notes/rl-1803.section.md b/nixos/doc/manual/release-notes/rl-1803.section.md
index e4e467981047b..c5146015d4499 100644
--- a/nixos/doc/manual/release-notes/rl-1803.section.md
+++ b/nixos/doc/manual/release-notes/rl-1803.section.md
@@ -282,3 +282,5 @@ When upgrading from a previous release, please be aware of the following incompa
 - The NixOS test driver supports user services declared by `systemd.user.services`. The methods `waitForUnit`, `getUnitInfo`, `startJob` and `stopJob` provide an optional `$user` argument for that purpose.
 
 - Enabling bash completion on NixOS, `programs.bash.enableCompletion`, will now also enable completion for the Nix command line tools by installing the [nix-bash-completions](https://github.com/hedning/nix-bash-completions) package.
+
+- The vim/kakoune plugin updater now reads from a CSV file: check `pkgs/applications/editors/vim/plugins/vim-plugin-names` out to see the new format
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 1b59842e020bb..e673d6721a38c 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -8,7 +8,22 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - Nix has been updated to version 2.4, reference its [release notes](https://discourse.nixos.org/t/nix-2-4-released/15822) for more information on what has changed. The previous version of Nix, 2.3.16, remains available for the time being in the `nix_2_3` package.
 
-- `iptables` now uses `nf_tables` backend.
+- `iptables` is now using `nf_tables` under the hood, by using `iptables-nft`,
+  similar to [Debian](https://wiki.debian.org/nftables#Current_status) and
+  [Fedora](https://fedoraproject.org/wiki/Changes/iptables-nft-default).
+  This means, `ip[6]tables`, `arptables` and `ebtables` commands  will actually
+  show rules from some specific tables in the `nf_tables` kernel subsystem.
+  In case you're migrating from an older release without rebooting, there might
+  be cases where you end up with iptable rules configured both in the legacy
+  `iptables` kernel backend, as well as in the `nf_tables` backend.
+  This can lead to confusing firewall behaviour. An `iptables-save` after
+  switching will complain about "iptables-legacy tables present".
+  It's probably best to reboot after the upgrade, or manually removing all
+  legacy iptables rules (via the `iptables-legacy` package).
+
+- systemd got an `nftables` backend, and configures (networkd) rules in their
+  own `io.systemd.*` tables. Check `nft list ruleset` to see these rules, not
+  `iptables-save` (which only shows `iptables`-created rules.
 
 - PHP now defaults to PHP 8.0, updated from 7.4.
 
@@ -151,7 +166,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 ## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
 
-- The NixOS VM test framework, `pkgs.nixosTest`/`make-test-python.nix`, now requires detaching commands such as `succeed("foo &")` and `succeed("foo | xclip -i")` to close stdout.
+- The NixOS VM test framework, `pkgs.nixosTest`/`make-test-python.nix` (`pkgs.testers.nixosTest` since 22.05), now requires detaching commands such as `succeed("foo &")` and `succeed("foo | xclip -i")` to close stdout.
   This can be done with a redirect such as `succeed("foo >&2 &")`. This breaking change was necessitated by a race condition causing tests to fail or hang.
   It applies to all methods that invoke commands on the nodes, including `execute`, `succeed`, `fail`, `wait_until_succeeds`, `wait_until_fails`.
 
@@ -419,6 +434,9 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The Linux kernel for security reasons now restricts access to BPF syscalls via `BPF_UNPRIV_DEFAULT_OFF=y`. Unprivileged access can be reenabled via the `kernel.unprivileged_bpf_disabled` sysctl knob.
 
+- `/usr` will always be included in the initial ramdisk. See the `fileSystems.<name>.neededForBoot` option.
+  If any files exist under `/usr` (which is not typical for NixOS), they will be included in the initial ramdisk, increasing its size to a possibly problematic extent.
+
 ## Other Notable Changes {#sec-release-21.11-notable-changes}
 
 
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index c748d2dae9e25..e83a7cd43b876 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -1,69 +1,177 @@
-# Release 22.05 (“Quokka”, 2022.05/??) {#sec-release-22.05}
-
-In addition to numerous new and upgraded packages, this release has the following highlights:
+# Release 22.05 (“Quokka”, 2022.05/30) {#sec-release-22.05}
 
 - Support is planned until the end of December 2022, handing over to 22.11.
 
 ## Highlights {#sec-release-22.05-highlights}
 
-- `security.acme.defaults` has been added to simplify configuring
-  settings for many certificates at once. This also opens up the
-  the option to use DNS-01 validation when using `enableACME` on
-  web server virtual hosts (e.g. `services.nginx.virtualHosts.*.enableACME`).
+In addition to numerous new and upgraded packages, this release has the following highlights:
 
-- PHP 8.1 is now available
+- Nix has been updated from 2.3 to 2.8. This mainly brings experimental support
+  for Flakes, but also marks the `nix` command as experimental which now has to
+  be enabled via the configuration explicitly. For more information and
+  instructions for upgrades, see the 
+  relase notes for [nix-2.4](https://nixos.org/manual/nix/stable/release-notes/rl-2.4.html),  
+  [nix-2.5](https://nixos.org/manual/nix/stable/release-notes/rl-2.5.html),
+  [nix-2.6](https://nixos.org/manual/nix/stable/release-notes/rl-2.6.html),
+  [nix-2.7](https://nixos.org/manual/nix/stable/release-notes/rl-2.7.html) and
+  [nix-2.8](https://nixos.org/manual/nix/stable/release-notes/rl-2.8.html)
 
-- Mattermost has been updated to extended support release 6.3, as the previously packaged extended support release 5.37 is [reaching its end of life](https://docs.mattermost.com/upgrade/extended-support-release.html).
-  Migrations may take a while, see the [changelog](https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release)
-  and [important upgrade notes](https://docs.mattermost.com/upgrade/important-upgrade-notes.html).
+- The `firefox` browser on `x86_64-linux` now makes use of profile-guided
+  optimisation, resulting in a much more responsive browsing experience.
+
+- GNOME has been upgraded to 42. Please take a look at their [Release
+  Notes](https://release.gnome.org/42/) for details. In particular, it replaces
+  gedit with GNOME Text Editor, GNOME Terminal with GNOME Console (formerly
+  King's Cross) and GNOME Screenshot by a tool integrated into the Shell.
+
+- PHP 8.1 is now available.
+
+- systemd services can now set [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services) instead of `reloadIfChanged` for a more granular distinction between reloads and restarts.
+
+- Systemd has been upgraded to the version 250.
+
+- Pulseaudio has been updated to version 15.0 and now optionally 
+  [supports additional Bluetooth audio codecs](https://www.freedesktop.org/wiki/Software/PulseAudio/Notes/15.0/#supportforldacandaptxbluetoothcodecsplussbcxqsbcwithhigher-qualityparameters)
+  such as aptX or LDAC, with codec switching available in `pavucontrol`. This
+  feature is disabled by default, but can be enabled with the option
+  `hardware.pulseaudio.package = pkgs.pulseaudioFull;`. Existing third-party
+  modules that offered similar functions, such as `pulseaudio-modules-bt` or
+  `pulseaudio-hsphfpd`, are obsolete and have been removed.
+
+- PostgreSQL now defaults to major version 14.
 
+- Module authors can use `mkRenamedOptionModuleWith` to automate the deprecation cycle without annoying out-of-tree module authors and their users.
+
+- The default GHC version has been updated from 8.10.7 to 9.0.2. `pkgs.haskellPackages` and `pkgs.ghc` will now use this version by default.
+
+- The GNOME and Plasma installation CDs now use `pkgs.calamares` and `pkgs.calamares-nixos-extensions` to allow users to easily install and set up NixOS with a GUI.
+
+- `security.acme.defaults` has been added to simplify the configuration of
+  settings for many certificates at once. This also opens up the option to use
+  DNS-01 validation when using `enableACME` web server virtual hosts (e.g.
+  `services.nginx.virtualHosts.*.enableACME`).
+  
 ## New Services {#sec-release-22.05-new-services}
 
+- [1password](https://1password.com/), command-lines and graphic interface for 1Password. Available as [programs._1password](#opt-programs._1password.enable) and [programs._1password-gui](#opt-programs._1password.enable).
+
 - [aesmd](https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw), the Intel SGX Architectural Enclave Service Manager. Available as [services.aesmd](#opt-services.aesmd.enable).
 
-- [rootless Docker](https://docs.docker.com/engine/security/rootless/), a `systemd --user` Docker service which runs without root permissions. Available as [virtualisation.docker.rootless.enable](options.html#opt-virtualisation.docker.rootless.enable).
+- [agate](https://github.com/mbrubeck/agate), a very simple server for the Gemini hypertext protocol. Available as [services.agate](#opt-services.agate.enable).
 
-- [matrix-conduit](https://conduit.rs/), a simple, fast and reliable chat server powered by matrix. Available as [services.matrix-conduit](option.html#opt-services.matrix-conduit.enable).
+- [apfs](https://github.com/linux-apfs/linux-apfs-rw), a kernel module for mounting the Apple File System (APFS).
+
+- [argonone](https://gitlab.com/DarkElvenAngel/argononed), a replacement daemon for the Raspberry Pi Argon One power button and cooler. Available at [services.hardware.argonone](options.html#opt-services.hardware.argonone.enable).
+
+- [ArchiSteamFarm](https://github.com/JustArchiNET/ArchiSteamFarm), a C# application with primary purpose of idling Steam cards from multiple accounts simultaneously. Available as [services.archisteamfarm](#opt-services.archisteamfarm.enable).
+
+- [BaGet](https://loic-sharma.github.io/BaGet/), a lightweight NuGet and symbol server. Available at [services.baget](#opt-services.baget.enable).
+
+- [bird-lg](https://github.com/xddxdd/bird-lg-go), a BGP looking glass for Bird Routing. Available as [services.bird-lg](#opt-services.bird-lg.package).
+
+- [blocky](https://0xerr0r.github.io/blocky/), fast and lightweight DNS proxy as ad-blocker for local network with many features. Available as [services.blocky](#opt-services.blocky.enable).
+
+- [cloudflare-dyndns](https://github.com/kissgyorgy/cloudflare-dyndns), CloudFlare Dynamic DNS client. Available as [services.cloudflare-dyndns](#opt-services.cloudflare-dyndns.enable).
+
+- [Corosync](https://corosync.github.io/corosync/) and [Pacemaker](https://clusterlabs.org/pacemaker/), A open-source high availability resource manager. Available as [services.corosync](#opt-services.corosync.enable) and [services.pacemaker](#opt-services.pacemaker.enable).
+
+- [create_ap](https://github.com/lakinduakash/linux-wifi-hotspot), a module for creating wifi hotspots using the program linux-wifi-hotspot. Available as [services.create_ap](#opt-services.create_ap.enable).
+
+- [Envoy](https://www.envoyproxy.io/), a high-performance reverse proxy. Available as [services.envoy](#opt-services.envoy.enable).
+
+- [ergochat](https://ergo.chat), a modern IRC with IRCv3 features. Available as [services.ergochat](#opt-services.ergochat.enable).
+
+- [ethercalc](https://github.com/audreyt/ethercalc), an online collaborative spreadsheet. Available as [services.ethercalc](#opt-services.ethercalc.enable).
 
 - [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable).
 
-- [apfs](https://github.com/linux-apfs/linux-apfs-rw), a kernel module for mounting the Apple File System (APFS).
+- [FRRouting](https://frrouting.org/), a popular suite of Internet routing protocol daemons (BGP, BFD, OSPF, IS-IS, VRRP and others). Available as [services.frr](#opt-services.frr.babel.enable).
 
-- [FRRouting](https://frrouting.org/), a popular suite of Internet routing protocol daemons (BGP, BFD, OSPF, IS-IS, VVRP and others). Available as [services.frr](#opt-services.ffr.babel.enable)
+- [Grafana Mimir](https://grafana.com/oss/mimir/), an open source, horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus. Available as [services.mimir](#opt-services.mimir.enable).
 
-- [heisenbridge](https://github.com/hifi/heisenbridge), a bouncer-style Matrix IRC bridge. Available as [services.heisenbridge](options.html#opt-services.heisenbridge.enable).
+- [Haste](https://hastebin.com/about.md), a pastebin written in node.js. Available as [services.haste](#opt-services.haste-server.enable).
 
-- [ergochat](https://ergo.chat), a modern IRC with IRCv3 features. Available as [services.ergochat](options.html#opt-services.ergochat.enable).
+- [headscale](https://github.com/juanfont/headscale), an Open Source implementation of the [Tailscale](https://tailscale.io) Control Server. Available as [services.headscale](#opt-services.headscale.enable).
 
-- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
+- [heisenbridge](https://github.com/hifi/heisenbridge), a bouncer-style Matrix IRC bridge. Available as [services.heisenbridge](#opt-services.heisenbridge.enable).
 
-- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](options.html#opt-services.invoiceplane.enable).
+- [https-dns-proxy](https://github.com/aarond10/https_dns_proxy), DNS to DNS over HTTPS (DoH) proxy. Available as [services.https-dns-proxy](#opt-services.https-dns-proxy.enable).
 
-- [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable).
+- [input-remapper](https://github.com/sezanzeb/input-remapper), an easy to use tool to change the mapping of your input device buttons. Available at [services.input-remapper](#opt-services.input-remapper.enable).
 
-- [mtr-exporter](https://github.com/mgumz/mtr-exporter), a Prometheus exporter for mtr metrics. Available as [services.mtr-exporter](options.html#opt-services.mtr-exporter.enable).
+- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](#opt-services.invoiceplane.sites._name_.enable).
 
-- [tetrd](https://tetrd.app), share your internet connection from your device to your PC and vice versa through a USB cable. Available at [services.tetrd](#opt-services.tetrd.enable).
+- [k3b](https://userbase.kde.org/K3b), the KDE disk burning application. Available as [programs.k3b](#opt-programs.k3b.enable).
 
-- [ArchiSteamFarm](https://github.com/JustArchiNET/ArchiSteamFarm), a C# application with primary purpose of idling Steam cards from multiple accounts simultaneously. Available as [services.archisteamfarm](options.html#opt-services.archisteamfarm.enable).
+- [K40-Whisperer](https://www.scorchworks.com/K40whisperer/k40whisperer.html), a program to control cheap Chinese laser cutters. Available as [programs.k40-whisperer.enable](#opt-programs.k40-whisperer.enable). Users must add themselves to the `k40` group to be able to access the device.
 
-- [teleport](https://goteleport.com), allows engineers and security professionals to unify access for SSH servers, Kubernetes clusters, web applications, and databases across all environments. Available at [services.teleport](#opt-services.teleport.enable).
+- [kanidm](https://kanidm.github.io/kanidm/stable/), an identity management server written in Rust. Available as [services.kanidm](#opt-services.kanidm.enableServer)
 
-- [BaGet](https://loic-sharma.github.io/BaGet/), a lightweight NuGet and symbol server. Available at [services.baget](#opt-services.baget.enable).
+- [Maddy](https://maddy.email/), a free an open source mail server. Availabe as [services.maddy](#opt-services.maddy.enable).
+
+- [matrix-conduit](https://conduit.rs/), a simple, fast and reliable chat server powered by matrix. Available as [services.matrix-conduit](option.html#opt-services.matrix-conduit.enable).
+
+- [Moosefs](https://moosefs.com), fault tolerant petabyte distributed file system. Available as [moosefs](#opt-services.moosefs.master.enable).
+
+- [mozillavpn](https://github.com/mozilla-mobile/mozilla-vpn-client), the client for the [Mozilla VPN](https://vpn.mozilla.org/) service. Available as [services.mozillavpn](#opt-services.mozillavpn.enable).
+
+- [mtr-exporter](https://github.com/mgumz/mtr-exporter), a Prometheus exporter for mtr metrics. Available as [services.mtr-exporter](#opt-services.mtr-exporter.enable).
+
+- [nbd](https://nbd.sourceforge.io/), a Network Block Device server. Available as [services.nbd](#opt-services.nbd.server.enable).
+
+- [netbox](https://github.com/netbox-community/netbox), infrastructure resource modeling (IRM) tool. Available as [services.netbox](#opt-services.netbox.enable).
 
-- [moosefs](https://moosefs.com), fault tolerant petabyte distributed file system.
-  Available as [moosefs](#opt-services.moosefs).
+- [nethoscope](https://github.com/vvilhonen/nethoscope), listen to your network traffic. Available as [programs.nethoscope](#opt-programs.nethoscope.enable).
+
+- [nifi](https://nifi.apache.org), an easy to use, powerful, and reliable system to process and distribute data. Available as [services.nifi](#opt-services.nifi.enable).
+
+- [nix-ld](https://github.com/Mic92/nix-ld), Run unpatched dynamic binaries on NixOS. Available as [programs.nix-ld](#opt-programs.nix-ld.enable).
+
+- [NNCP](http://www.nncpgo.org), NNCP (Node to Node copy) utilities and configuration, Available as [programs.nncp](#opt-programs.nncp.enable).
+
+- [pgadmin4](https://github.com/postgres/pgadmin4), an admin interface for the PostgreSQL database. Available at [services.pgadmin](#opt-services.pgadmin.enable).
+
+- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](#opt-services.powerdns-admin.enable).
+
+- [prometheus-pve-exporter](https://github.com/prometheus-pve/prometheus-pve-exporter), a tool that exposes information from the Proxmox VE API for use by Prometheus. Available as [services.prometheus.exporters.pve](#opt-services.prometheus.exporters.pve.enable).
 
 - [prosody-filer](https://github.com/ThomasLeister/prosody-filer), a server for handling XMPP HTTP Upload requests. Available at [services.prosody-filer](#opt-services.prosody-filer.enable).
 
-- [ethercalc](https://github.com/audreyt/ethercalc), an online collaborative
-  spreadsheet. Available as [services.ethercalc](options.html#opt-services.ethercalc.enable).
+- [Public Inbox](https://public-inbox.org), an "archives first" approach to mailing lists. Available as [services.public-inbox](#opt-services.public-inbox.enable).
+
+- [r53-ddns](https://github.com/fleaz/r53-ddns), a small tool to run your own DDNS service via AWS Route53. Available as [services.r53-ddns](#opt-services.r53-ddns.enable).
+
+- [rmfakecloud](https://ddvk.github.io/rmfakecloud/), a clone of the cloud sync the remarkable tablet. Available as [services.rmfakecloud](#opt-services.rmfakecloud.enable).
+
+- [rootless Docker](https://docs.docker.com/engine/security/rootless/), a `systemd --user` Docker service which runs without root permissions. Available as [virtualisation.docker.rootless.enable](#opt-virtualisation.docker.rootless.enable).
+
+- [rstudio-server](https://www.rstudio.com/products/rstudio/#rstudio-server), a browser-based version of the RStudio IDE for the R programming language. Available as [services.rstudio-server](#opt-services.rstudio-server.enable).
+
+- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server), ready-to-use RTSP / RTMP / HLS server and proxy that allows to read, publish and proxy video and audio streams. Available as [services.rtsp-simple-server](#opt-services.rtsp-simple-server.enable).
+
+- [Snipe-IT](https://snipeitapp.com), a free open source IT asset/license management system. Available as [services.snipe-it](#opt-services.snipe-it.enable).
+
+- [snowflake-proxy](https://snowflake.torproject.org/), a system to defeat internet censorship. Available as [services.snowflake-proxy](#opt-services.snowflake-proxy.enable).
+
+- [sslmate-agent](https://sslmate.com/), a daemon for managing SSL/TLS certificates on a server. Available as [services.sslmate-agent](services.sslmate-agent.enable).
+
+- [starship](https://starship.rs), a minimal, blazing-fast, and infinitely customizable prompt for any shell. Available at [programs.startship](#opt-programs.starship.enable).
+
+- [systembus-notify](https://github.com/rfjakob/systembus-notify), allow system level notifications to reach the users. Available as [services.systembus-notify](opt-services.systembus-notify.enable). Please keep in mind that this service should only be enabled on machines with fully trusted users, as any local user is able to DoS user sessions by spamming notifications.
+
+- [teleport](https://goteleport.com), allows engineers and security professionals to unify access for SSH servers, Kubernetes clusters, web applications, and databases across all environments. Available at [services.teleport](#opt-services.teleport.enable).
+
+- [tetrd](https://tetrd.app), share your internet connection from your device to your PC and vice versa through a USB cable. Available at [services.tetrd](#opt-services.tetrd.enable).
 
-- [timetagger](https://timetagger.app), an open source time-tracker with an intuitive user experience and powerful reporting. [services.timetagger](options.html#opt-services.timetagger.enable).
+- [uptermd](https://upterm.dev), an open-source solution for sharing terminal sessions instantly over the public internet via secure tunnels. Available at [services.uptermd](#opt-services.uptermd.enable).
 
-- [rstudio-server](https://www.rstudio.com/products/rstudio/#rstudio-server), a browser-based version of the RStudio IDE for the R programming language. Available as [services.rstudio-server](options.html#opt-services.rstudio-server.enable).
+- [usbrelayd](https://github.com/darrylb123/usbrelay), an USB Relay MQTT daemon. Available as [services.usbrelayd](#opt-services.usbrelayd.enable).
 
-- [headscale](https://github.com/juanfont/headscale), an Open Source implementation of the [Tailscale](https://tailscale.io) Control Server. Available as [services.headscale](options.html#opt-services.headscale.enable)
+- [webdav-server-rs](https://github.com/miquels/webdav-server-rs), Webdav server in rust. Available as [services.webdav-server-rs](#opt-services.webdav-server-rs.enable).
+
+- [wg-netmanager](https://github.com/gin66/wg_netmanager), the Wireguard network manager. Available as [services.wg-netmanager](#opt-services.wg-netmanager.enable).
+
+- [Zammad](https://zammad.org/), a web-based, open source user support/ticketing solution. Available as [services.zammad](#opt-services.zammad.enable).
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
@@ -87,13 +195,114 @@ In addition to numerous new and upgraded packages, this release has the followin
   `useLLVM`. So instead of `(ghc.withPackages (p: [])).override { withLLVM = true; }`,
   one needs to use `(ghc.withPackages.override { useLLVM = true; }) (p: [])`.
 
+- The update of the haskell package set brings with it a new version of the `xmonad`
+  module, which will break your configuration if you use `launch` as entrypoint. The
+  example code the corresponding nixos module was adjusted, you may want to have a look at it.
+
+- The `home-assistant` module now requires users that don't want their
+  configuration to be managed declaratively to set
+  `services.home-assistant.config = null;`. This is required
+  due to the way default settings are handled with the new settings style.
+
+  Additionally the default list of `extraComponents` now includes the minimal
+  dependencies to successfully complete the [onboarding](https://www.home-assistant.io/getting-started/onboarding/)
+  procedure.
+
 - `pkgs.emacsPackages.orgPackages` is removed because org elpa is deprecated.
   The packages in the top level of `pkgs.emacsPackages`, such as org and
   org-contrib, refer to the ones in `pkgs.emacsPackages.elpaPackages` and
   `pkgs.emacsPackages.nongnuPackages` where the new versions will release.
 
+- The configuration and state directories used by `nixos-containers` have been
+  moved from `/etc/containers` and `/var/lib/containers` to
+  `/etc/nixos-containers` and `/var/lib/nixos-containers`.
+
+  If you are changing `system.stateVersion` to `"22.05"` manually on an existing
+  system you are responsible for migrating these directories yourself.
+
+  This is to improve compatibility with `libcontainer` based software such as Podman and Skopeo
+  which assumes they have ownership over `/etc/containers`.
+
+- `lib.systems.supported` has been removed, as it was overengineered for determining the systems to support in the nixpkgs flake. The list of systems exposed by the nixpkgs flake can now be accessed as `lib.systems.flakeExposed`.
+
+- For new installations `virtualisation.oci-containers.backend` is now set to `podman` by default.
+  If you still want to use Docker on systems where `system.stateVersion` is set to to `"22.05"` set `virtualisation.oci-containers.backend = "docker";`.Old systems with older `stateVersion`s stay with "docker".
+
+- `security.klogd` was removed.  Logging of kernel messages is handled
+  by systemd since Linux 3.5.
+
+- `pkgs.ssmtp` has been dropped due to the program being unmaintained.
+  `pkgs.msmtp` can be used instead as a substitute `sendmail` implementation.
+  The corresponding options `services.ssmtp.*` have been removed as well.
+  `programs.msmtp.*` can be used instead for an equivalent setup. For example:
+
+  ```nix
+  {
+    # Original ssmtp configuration:
+    services.ssmtp = {
+      enable = true;
+      useTLS = true;
+      useSTARTTLS = true;
+      hostName = "smtp.example:587";
+      authUser = "someone";
+      authPassFile = "/secrets/password.txt";
+    };
+
+    # Equivalent msmtp configuration:
+    programs.msmtp = {
+      enable = true;
+      accounts.default = {
+        tls = true;
+        tls_starttls = true;
+        auth = true;
+        host = "smtp.example";
+        port = 587;
+        user = "someone";
+        passwordeval = "cat /secrets/password.txt";
+      };
+    };
+  }
+  ```
+
 - `services.kubernetes.addons.dashboard` was removed due to it being an outdated version.
 
+- `services.kubernetes.scheduler.{port,address}` now set `--secure-port` and `--bind-address` instead of `--port` and `--address`, since the former have been deprecated and are no longer functional in kubernetes>=1.23. Ensure that you are not relying on the insecure behaviour before upgrading.
+
+- In the PowerDNS Recursor module (`services.pdns-recursor`), default values of several IP address-related NixOS options have been updated to match the default upstream behavior.
+  In particular, Recursor by default will:
+    - listen on (and allows connections from) both IPv4 and IPv6 addresses
+      (`services.pdns-recursor.dns.address`, `services.pdns-recursor.dns.allowFrom`);
+    - allow only local connections to the REST API server (`services.pdns-recursor.api.allowFrom`).
+
+- In the ncdns module, the default value of `services.ncdns.address` has been changed to the IPv6 loopback address (`::1`).
+
+- `openldap` (and therefore the slapd LDAP server) were updated to version 2.6.2. The project introduced backwards-incompatible changes, namely the removal of the bdb, hdb, ndb, and shell backends in slapd. Therefore before updating, dump your database `slapcat -n 1` in LDIF format, and reimport it after updating your `services.openldap.settings`, which represents your `cn=config`.
+
+  Additionally with 2.5 the argon2 module was included in the standard distrubtion and renamed from `pw-argon2` to `argon2`. Remember to update your `olcModuleLoad` entry in `cn=config`.
+
+- `openssh` has been update to 8.9p1, changing the FIDO security key middleware interface.
+
+- `git` no longer hardcodes the path to openssh' ssh binary to reduce the amount of rebuilds. If you are using git with ssh remotes and do not have a ssh binary in your enviroment consider adding `openssh` to it or switching to `gitFull`.
+
+- `services.k3s.enable` no longer implies `systemd.enableUnifiedCgroupHierarchy = false`, and will default to the 'systemd' cgroup driver when using `services.k3s.docker = true`.
+  This change may require a reboot to take effect, and k3s may not be able to run if the boot cgroup hierarchy does not match its configuration.
+  The previous behavior may be retained by explicitly setting `systemd.enableUnifiedCgroupHierarchy = false` in your configuration.
+
+- `fonts.fonts` no longer includes ancient bitmap fonts when both `config.services.xserver.enable` and `config.nixpkgs.config.allowUnfree` are enabled.
+  If you still want these fonts, use:
+
+  ```nix
+  {
+    fonts.fonts = [
+      pkgs.xorg.fontbhlucidatypewriter100dpi
+      pkgs.xorg.fontbhlucidatypewriter75dpi
+      pkgs.xorg.fontbh100dpi
+    ];
+  }
+  ```
+
+- `services.prometheus.alertManagerTimeout` has been removed as it has been deprecated upstream and has no effect.
+
 - The DHCP server (`services.dhcpd4`, `services.dhcpd6`) has been hardened.
   The service is now using the systemd's `DynamicUser` mechanism to run as an unprivileged dynamically-allocated user with limited capabilities.
   The dhcpd state files are now always stored in `/var/lib/dhcpd{4,6}` and the `services.dhcpd4.stateDir` and `service.dhcpd6.stateDir` options have been removed.
@@ -101,8 +310,193 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The `mailpile` email webclient (`services.mailpile`) has been removed due to its reliance on python2.
 
+- `services.ipfs.extraFlags` is now escaped with `utils.escapeSystemdExecArgs`. If you rely on systemd interpolating `extraFlags` in the service `ExecStart`, this will no longer work.
+
+- `hbase` version 0.98.24 has been removed. The package now defaults to version 2.4.11. Versions 1.7.1 and 3.0.0-alpha-2 are also available.
+
+- `services.paperless-ng` was renamed to `services.paperless`. Accordingly, the `paperless-ng-manage` script (located in `dataDir`) was renamed to `paperless-manage`. `services.paperless` now uses `paperless-ngx`.
+
+- The `matrix-synapse` service (`services.matrix-synapse`) has been converted to use the `settings` option defined in RFC42.
+  This means that options that are part of your `homeserver.yaml` configuration, and that were specified at the top-level of the
+  module (`services.matrix-synapse`) now need to be moved into `services.matrix-synapse.settings`. And while not all options you
+  may use are defined in there, they are still supported, because you can set arbitrary values in this freeform type.
+
+  The `listeners.*.bind_address` option was renamed to `bind_addresses` in order to match the upstream `homeserver.yaml` option
+  name. It is now also a list of strings instead of a string.
+
+  An example to make the required migration clearer:
+
+  Before:
+  ```nix
+  {
+    services.matrix-synapse = {
+      enable = true;
+
+      server_name = "example.com";
+      public_baseurl = "https://example.com:8448";
+
+      enable_registration = false;
+      registration_shared_secret = "xohshaeyui8jic7uutuDogahkee3aehuaf6ei3Xouz4iicie5thie6nohNahceut";
+      macaroon_secret_key = "xoo8eder9seivukaiPh1cheikohquuw8Yooreid0The4aifahth3Ou0aiShaiz4l";
+
+      tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+      tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+
+      listeners = [ {
+        port = 8448;
+        bind_address = "";
+        type = "http";
+        tls = true;
+        resources = [ {
+          names = [ "client" ];
+          compress = true;
+        } {
+          names = [ "federation" ];
+          compress = false;
+        } ];
+      } ];
+
+    };
+  }
+  ```
+
+  After:
+  ```nix
+  {
+    services.matrix-synapse = {
+      enable = true;
+
+      # this attribute set holds all values that go into your homeserver.yaml configuration
+      # See https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml for
+      # possible values.
+      settings = {
+        server_name = "example.com";
+        public_baseurl = "https://example.com:8448";
+
+        enable_registration = false;
+        # pass `registration_shared_secret` and `macaroon_secret_key` via `extraConfigFiles` instead
+
+        tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+        tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+
+        listeners = [ {
+          port = 8448;
+          bind_addresses = [
+            "::"
+            "0.0.0.0"
+          ];
+          type = "http";
+          tls = true;
+          resources = [ {
+            names = [ "client" ];
+            compress = true;
+          } {
+            names = [ "federation" ];
+            compress = false;
+          } ];
+        } ];
+      };
+
+      extraConfigFiles = [
+        "/run/keys/matrix-synapse/secrets.yaml"
+      ];
+    };
+  }
+  ```
+
+  The secrets in your original config should be migrated into a YAML file that is included via `extraConfigFiles`. The filename must be quoted to prevent nix from copying it to the (world readable) store.
+
+  Additionally a few option defaults have been synced up with upstream default values, for example the `max_upload_size` grew from `10M` to `50M`. For the same reason, the default
+  `media_store_path` was changed from `${dataDir}/media` to `${dataDir}/media_store` if `system.stateVersion` is at least `22.05`. Files will need to be manually moved to the new
+  location if the `stateVersion` is updated.
+
+  As of Synapse 1.58.0, the old groups/communities feature has been disabled by default. It will be completely removed with Synapse 1.61.0.
+
+- The Keycloak package (`pkgs.keycloak`) has been switched from the
+  Wildfly version, which will soon be deprecated, to the Quarkus based
+  version. The Keycloak service (`services.keycloak`) has been updated
+  to accommodate the change and now differs from the previous version
+  in a few ways:
+
+  - `services.keycloak.extraConfig` has been removed in favor of the
+    new [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md)
+    [`services.keycloak.settings`](#opt-services.keycloak.settings)
+    option. The available options correspond directly to parameters in
+    `conf/keycloak.conf`. Some of the most important parameters are
+    documented as suboptions, the rest can be found in the [All
+    configuration section of the Keycloak Server Installation and
+    Configuration
+    Guide](https://www.keycloak.org/server/all-config). While the new
+    configuration is much simpler and cleaner than the old JBoss CLI
+    one, this unfortunately mean that there's no straightforward way
+    to convert an old configuration to the new format and some
+    settings may not even be available anymore.
+
+  - `services.keycloak.frontendUrl` was removed and the frontend URL
+    is now configured through the `hostname` family of settings in
+    [`services.keycloak.settings`](#opt-services.keycloak.settings)
+    instead. See the [Hostname section of the Keycloak Server
+    Installation and Configuration
+    Guide](https://www.keycloak.org/server/hostname) for more
+    details. Additionally, `/auth` was removed from the default
+    context path and needs to be added back in
+    [`services.keycloak.settings.http-relative-path`](#opt-services.keycloak.settings.http-relative-path)
+    if you want to keep compatibility with your current clients.
+
+  - `services.keycloak.bindAddress`,
+    `services.keycloak.forceBackendUrlToFrontendUrl`,
+    `services.keycloak.httpPort` and `services.keycloak.httpsPort`
+    have been removed in favor of their equivalent options in
+    [`services.keycloak.settings`](#opt-services.keycloak.settings). `httpPort`
+    and `httpsPort` have additionally had their types changed from
+    `str` to `port`.
+
+    The new names are as follows:
+    - `bindAddress`: [`services.keycloak.settings.http-host`](#opt-services.keycloak.settings.http-host)
+    - `forceBackendUrlToFrontendUrl`: [`services.keycloak.settings.hostname-strict-backchannel`](#opt-services.keycloak.settings.hostname-strict-backchannel)
+    - `httpPort`: [`services.keycloak.settings.http-port`](#opt-services.keycloak.settings.http-port)
+    - `httpsPort`: [`services.keycloak.settings.https-port`](#opt-services.keycloak.settings.https-port)
+
+  For example, when using a reverse proxy the migration could look
+  like this:
+
+  Before:
+  ```nix
+    services.keycloak = {
+      enable = true;
+      httpPort = "8080";
+      frontendUrl = "https://keycloak.example.com/auth";
+      database.passwordFile = "/run/keys/db_password";
+      extraConfig = {
+        "subsystem=undertow"."server=default-server"."http-listener=default".proxy-address-forwarding = true;
+      };
+    };
+  ```
+
+  After:
+  ```nix
+    services.keycloak = {
+      enable = true;
+      settings = {
+        http-port = 8080;
+        hostname = "keycloak.example.com";
+        http-relative-path = "/auth";
+        proxy = "edge";
+      };
+      database.passwordFile = "/run/keys/db_password";
+    };
+  ```
+
 - The MoinMoin wiki engine (`services.moinmoin`) has been removed, because Python 2 is being retired from nixpkgs.
 
+- Services in the `hadoop` module previously set `openFirewall` to true by default.
+  This has now been changed to false. Node definitions for multi-node clusters would need
+  `openFirewall = true;` to be added to to hadoop services when upgrading from NixOS 21.11.
+
+- `services.hadoop.yarn.nodemanager` now uses cgroup-based CPU limit enforcement by default.
+  Additionally, the option `useCGroups` was added to nodemanagers as an easy way to switch
+  back to the old behavior.
+
 - The `wafHook` hook now honors `NIX_BUILD_CORES` when `enableParallelBuilding` is not set explicitly. Packages can restore the old behaviour by setting `enableParallelBuilding=false`.
 
 - `pkgs.claws-mail-gtk2`, representing Claws Mail's older release version three, was removed in order to get rid of Python 2.
@@ -115,8 +509,14 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The `gnome-passwordsafe` package updated to [version 6.x](https://gitlab.gnome.org/World/secrets/-/tags/6.0) and renamed to `gnome-secrets`.
 
+- `services.gnome.experimental-features.realtime-scheduling` option has been removed, as GNOME Shell now [uses rtkit](https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2060). Use `security.rtkit.enable = true;` instead. As before, you will need to have it enabled using GSettings.
+
+- `services.telepathy` will no longer be enabled by default for GNOME desktops, one should enable it in their configs if using Empathy or Polari.
+
 - If you previously used `/etc/docker/daemon.json`, you need to incorporate the changes into the new option `virtualisation.docker.daemon.settings`.
 
+- Ntopng (`services.ntopng`) is updated to 5.2.1 and uses a separate Redis instance if `system.stateVersion` is at least `22.05`. Existing setups shouldn't be affected.
+
 - The backward compatibility in `services.wordpress` to configure sites with
   the old interface has been removed. Please use `services.wordpress.sites`
   instead.
@@ -126,8 +526,16 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - opensmtpd-extras is no longer build with python2 scripting support due to python2 deprecation in nixpkgs
 
+- `services.miniflux.adminCredentialFiles` is now required, instead of defaulting to `admin` and `password`.
+
+- The `taskserver` module no longer implicitly opens ports in the firewall
+  configuration. This is now controlled through the option
+  `services.taskserver.openFirewall`.
+
 - The `autorestic` package has been upgraded from 1.3.0 to 1.5.0 which introduces breaking changes in config file, check [their migration guide](https://autorestic.vercel.app/migration/1.4_1.5) for more details.
 
+- `teleport` has been upgraded to major version 9. Please see upstream [upgrade instructions](https://goteleport.com/docs/setup/operations/upgrading/) and [release notes](https://goteleport.com/docs/changelog/#900).
+
 - For `pkgs.python3.pkgs.ipython`, its direct dependency `pkgs.python3.pkgs.matplotlib-inline`
   (which is really an adapter to integrate matplotlib in ipython if it is installed) does
   not depend on `pkgs.python3.pkgs.matplotlib` anymore.
@@ -151,18 +559,53 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `pkgs.docbookrx` was removed since it's unmaintained
 
+- `pkgs._7zz` is now correctly licensed as LGPL3+ and BSD3 with optional unfree unRAR licensed code
+
+- The `vim.customize` function produced by `vimUtils.makeCustomizable` now has a slightly different interface:
+  * The wrapper now includes everything in the given Vim derivation if `name` is `"vim"` (the default). This makes the `wrapManual` argument obsolete, but this behavior can be overriden by setting the `standalone` argument.
+  * All the executables present in the given derivation (or, in `standalone` mode, only the `*vim` ones) are wrapped. This makes the `wrapGui` argument obsolete.
+  * The `vimExecutableName` and `gvimExecutableName` arguments were replaced by a single `executableName` argument in which the shell variable `$exe` can be used to refer to the wrapped executable's name.
+
+  See the comments in `pkgs/applications/editors/vim/plugins/vim-utils.nix` for more details.
+
+  `vimUtils.vimWithRC` was removed. You should instead use `customize` on a Vim derivation, which now accepts `vimrcFile` and `gvimrcFile` arguments.
+
 - `tilp2` was removed together with its module
 
+- The F-PROT antivirus (`fprot` package) and its service module were removed because it
+  reached [end-of-life](https://kb.cyren.com/av-support/index.php?/Knowledgebase/Article/View/434/0/end-of-sale--end-of-life-for-f-prot-and-csam).
+
+- `bird1` and its modules `services.bird` as well as `services.bird6` have been removed. Upgrade to `services.bird2`.
+
 - The options `networking.interfaces.<name>.ipv4.routes` and `networking.interfaces.<name>.ipv6.routes` are no longer ignored when using networkd instead of the default scripted network backend by setting `networking.useNetworkd` to `true`.
 
+- The `miller` package has been upgraded from 5.10.3 to [6.2.0](https://github.com/johnkerl/miller/releases/tag/v6.2.0). See [What's new in Miller 6](https://miller.readthedocs.io/en/latest/new-in-miller-6).
+
 - MultiMC has been replaced with the fork PolyMC due to upstream developers being hostile to 3rd party package maintainers. PolyMC removes all MultiMC branding and is aimed at providing proper 3rd party packages like the one contained in Nixpkgs. This change affects the data folder where game instances and other save and configuration files are stored. Users with existing installations should rename `~/.local/share/multimc` to `~/.local/share/polymc`. The main config file's path has also moved from `~/.local/share/multimc/multimc.cfg` to `~/.local/share/polymc/polymc.cfg`.
 
+- `systemd-nspawn@.service` settings have been reverted to the default systemd behaviour. User namespaces are now activated by default. If you want to keep running nspawn containers without user namespaces you need to set `systemd.nspawn.<name>.execConfig.PrivateUsers = false`
+
+- `systemd-shutdown` is now properly linked on shutdown to unmount all filesystems and device mapper devices cleanly. This can be disabled using `systemd.shutdownRamfs.enable`.
+
+- The Tor SOCKS proxy is now actually disabled if `services.tor.client.enable` is set to `false` (the default). If you are using this functionality but didn't change the setting or set it to `false`, you now need to set it to `true`.
+
+- `services.github-runner` has been hardened.  Notably address families and
+  system calls have been restricted, which may adversely affect some kinds of
+  testing, e.g. using `AF_BLUETOOTH` to test bluetooth devices.
 
 - The terraform 0.12 compatibility has been removed and the `terraform.withPlugins` and `terraform-providers.mkProvider` implementations simplified. Providers now need to be stored under
 `$out/libexec/terraform-providers/<registry>/<owner>/<name>/<version>/<os>_<arch>/terraform-provider-<name>_v<version>` (which mkProvider does).
 
   This breaks back-compat so it's not possible to mix-and-match with previous versions of nixpkgs. In exchange, it now becomes possible to use the providers from [nixpkgs-terraform-providers-bin](https://github.com/numtide/nixpkgs-terraform-providers-bin) directly.
 
+- The `dendrite` package has been upgraded from 0.5.1 to
+  [0.6.5](https://github.com/matrix-org/dendrite/releases/tag/v0.6.5). Instances
+  configured with split sqlite databases, which has been the default
+  in NixOS, require merging of the federation sender and signing key
+  databases. See upstream [release
+  notes](https://github.com/matrix-org/dendrite/releases/tag/v0.6.0)
+  on version 0.6.0 for details on database changes.
+
 - The existing `pkgs.opentelemetry-collector` has been moved to
   `pkgs.opentelemetry-collector-contrib` to match the actual source being the
   "contrib" edition. `pkgs.opentelemetry-collector` is now the actual core
@@ -170,6 +613,13 @@ In addition to numerous new and upgraded packages, this release has the followin
   you should change the package you refer to. If you don't need them update your
   commands from `otelcontribcol` to `otelcorecol` and enjoy a 7x smaller binary.
 
+- `services.zookeeper` has a new option `jre` for specifying the JRE to start
+  zookeeper with. It defaults to the JRE that `pkgs.zookeeper` was wrapped with,
+  instead of `pkgs.jre`. This changes the JRE to `pkgs.jdk11_headless` by default.
+
+- `pkgs.pgadmin` now refers to `pkgs.pgadmin4`. `pgadmin3` has been removed.
+
+- `pkgs.minetestclient_4` and `pkgs.minetestserver_4` have been removed, as the last 4.x release was in 2018. `pkgs.minetestclient` (equivalent to `pkgs.minetest` ) and `pkgs.minetestserver` can be used instead.
 
 - `pkgs.noto-fonts-cjk` is now deprecated in favor of `pkgs.noto-fonts-cjk-sans`
   and `pkgs.noto-fonts-cjk-serif` because they each have different release
@@ -177,8 +627,33 @@ In addition to numerous new and upgraded packages, this release has the followin
   `pkgs.noto-fonts-cjk` is currently an alias of `pkgs.noto-fonts-cjk-sans` and
   doesn't include serif fonts.
 
+- `pkgs.epgstation` has been upgraded from v1 to v2, resulting in incompatible
+  changes in the database scheme and configuration format.
+
+- Some top-level settings under [services.epgstation](#opt-services.epgstation.enable)
+  is now deprecated because it was redudant due to the same options being
+  present in [services.epgstation.settings](#opt-services.epgstation.settings).
+
+- The option `services.epgstation.basicAuth` was removed because basic
+  authentication support was dropped by upstream.
+
+- The option [services.epgstation.database.passwordFile](#opt-services.epgstation.database.passwordFile)
+  no longer has a default value. Make sure to set this option explicitly before
+  upgrading. Change the database password if necessary.
+
+- The [services.epgstation.settings](#opt-services.epgstation.settings)
+  option now expects options for `config.yml` in EPGStation v2.
+
+- Existing data for the [services.epgstation](#opt-services.epgstation.enable)
+  module would have to be backed up prior to the upgrade. To back up exising
+  data to `/tmp/epgstation.bak`, run
+  `sudo -u epgstation epgstation run backup /tmp/epgstation.bak`.
+  To import that data after to the upgrade, run
+  `sudo -u epgstation epgstation run v1migrate /tmp/epgstation.bak`
+
 - `switch-to-configuration` (the script that is run when running `nixos-rebuild switch` for example) has been reworked
     * The interface that allows activation scripts to restart units has been streamlined. Restarting and reloading is now done by a single file `/run/nixos/activation-restart-list` that honors `restartIfChanged` and `reloadIfChanged` of the units.
+        * Preferring to reload instead of restarting can still be achieved using `/run/nixos/activation-reload-list`.
     * The script now uses a proper ini-file parser to parse systemd units. Some values are now only searched in one section instead of in the entire unit. This is only relevant for units that don't use the NixOS systemd moule.
         * `RefuseManualStop`, `X-OnlyManualStart`, `X-StopOnRemoval`, `X-StopOnReconfiguration` are only searched in the `[Unit]` section
         * `X-ReloadIfChanged`, `X-RestartIfChanged`, `X-StopIfChanged` are only searched in the `[Service]` section
@@ -193,9 +668,40 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `lib.assertMsg` and `lib.assertOneOf` no longer return `false` if the passed condition is `false`, `throw`ing the given error message instead (which makes the resulting error message less cluttered). This will not impact the behaviour of code using these functions as intended, namely as top-level wrapper for `assert` conditions.
 
+- The `vpnc` package has been changed to use GnuTLS instead of OpenSSL by default for licensing reasons.
+
+- The default version of `nextcloud` is **nextcloud24**. Please note that it's **not** possible to upgrade
+  `nextcloud` across multiple major versions! This means it's e.g. not possible to upgrade from `nextcloud22`
+  to `nextcloud24` in a single deploy and most `21.11` users will have to upgrade to `nextcloud23` first.
+
 - `pkgs.vimPlugins.onedark-nvim` now refers to [navarasu/onedark.nvim](https://github.com/navarasu/onedark.nvim)
   (formerly refers to [olimorris/onedarkpro.nvim](https://github.com/olimorris/onedarkpro.nvim)).
 
+- `services.pipewire.enable` will default to enabling the WirePlumber session manager instead of pipewire-media-session.
+  pipewire-media-session is deprecated by upstream and not recommended, but can still be manually enabled by setting
+  `services.pipewire.media-session.enable` to `true` and `services.pipewire.wireplumber.enable` to `false`.
+
+- `pkgs.makeDesktopItem` has been refactored to provide a more idiomatic API. Specifically:
+  - All valid options as of FDO Desktop Entry specification version 1.4 can now be passed in as explicit arguments
+  - `exec` can now be null, for entries that are not of type Application
+  - `mimeType` argument is renamed to `mimeTypes` for consistency
+  - `mimeTypes`, `categories`, `implements`, `keywords`, `onlyShowIn` and `notShowIn` take lists of strings instead of one string with semicolon separators
+  - `extraDesktopEntries` renamed to `extraConfig` for consistency
+  - Actions should now be provided as an attrset `actions`, the `Actions` line will be autogenerated.
+  - `extraEntries` is removed.
+  - Additional validation is added both at eval time and at build time.
+
+  See the `vscode` package for a more detailed example.
+
+- Existing `resholve*` functions have been renamed and nested under `pkgs.resholve`. Update uses to:
+  - `resholvePackage` -> `resholve.mkDerivation`
+  - `resholveScript` -> `resholve.writeScript`
+  - `resholveScriptBin` -> `resholve.writeScriptBin`
+
+- `pkgs.cosmopolitan` no longer provides the `cosmoc` command. It has been moved to `pkgs.cosmoc`.
+
+- `pkgs.graalvmXX-ce` packages no longer provide support for Python/Ruby/WASM, instead focusing only in Java and Native Image Support. If you need to add support back, please see the `pkgs.graalvmCEPackages.mkGraal` function to create your own customized version of GraalVM with support for what you need.
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Other Notable Changes {#sec-release-22.05-notable-changes}
@@ -227,11 +733,52 @@ In addition to numerous new and upgraded packages, this release has the followin
   Similarly [virtualisation.vmVariantWithBootloader](#opt-virtualisation.vmVariantWithBootLoader) was added.
 
 - The configuration portion of the `nix-daemon` module has been reworked and exposed as [nix.settings](options.html#opt-nix-settings):
-  * Legacy options have been mapped to the corresponding options under under [nix.settings](options.html#opt-nix.settings) but may be deprecated in the future.
+  * Legacy options have been mapped to the corresponding options under under [nix.settings](options.html#opt-nix.settings) and will be deprecated when NixOS 21.11 reaches end of life.
   * [nix.buildMachines.publicHostKey](options.html#opt-nix.buildMachines.publicHostKey) has been added.
+  
+- [`kops`](https://kops.sigs.k8s.io) defaults to 1.23.2, which will enable [Instance Metadata Service Version 2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) and require tokens on new clusters with Kubernetes >= 1.22. This will increase security by default, but may break some types of workloads. The default behaviour for `spec.kubeDNS.nodeLocalDNS.forwardToKubeDNS` has changed from `true` to `false`. Cilium now has `disable-cnp-status-updates: true` by default. Set this to false if you rely on the CiliumNetworkPolicy status fields. Support for Kubernetes 1.17, the Lyft CNI, Weave CNI on Kubernetes >= 1.23, CentOS 7 and 8, Debian 9, RHEL 7, and Ubuntu 16.05 (Xenial) has been removed. See the [1.22 release notes](https://kops.sigs.k8s.io/releases/1.22-notes/) and [1.23 release notes](https://kops.sigs.k8s.io/releases/1.23-notes/) for more details, including other significant changes.
+
+- Mattermost has been upgraded to extended support version 6.3 as the previously
+  packaged extended support version 5.37 is [reaching end of life](https://docs.mattermost.com/upgrade/extended-support-release.html). 
+  Migration may take some time, see the [changelog](https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release)
+  and [important upgrade notes](https://docs.mattermost.com/upgrade/important-upgrade-notes.html).
 
 - The `writers.writePyPy2`/`writers.writePyPy3` and corresponding `writers.writePyPy2Bin`/`writers.writePyPy3Bin` convenience functions to create executable Python 2/3 scripts using the PyPy interpreter were added.
 
+- Some improvements have been made to the `hadoop` module:
+  - A `gatewayRole` option has been added, for deploying hadoop cluster configuration files to a node that does not have any active services
+  - Support for older versions of hadoop have been added to the module
+  - Overriding and extending site XML files has been made easier
+
+- The auto-upgrade service now accepts persistent (default: true) parameter.
+  By default auto-upgrade will now run immediately if it would have been triggered at least
+  once during the time when the timer was inactive.
+
+- Mastodon now uses `services.redis.servers` to start a new redis server, instead of using a global redis server. 
+  This improves compatibility with other services that use redis.
+  
+  Note that this will recreate the redis database, although according to the [Mastodon docs](https://docs.joinmastodon.org/admin/backups/), 
+  this is almost harmless:
+  > Losing the Redis database is almost harmless: The only irrecoverable data will be the contents of the Sidekiq queues and scheduled retries of previously failed jobs. 
+  >  The home and list feeds are stored in Redis, but can be regenerated with tootctl.
+  
+  If you do want to save the redis database, you can use the following commands:
+  ```bash
+  redis-cli save
+  cp /var/lib/redis/dump.rdb "/var/lib/redis-mastodon/dump.rdb"
+  ```
+- Peertube now uses services.redis.servers to start a new redis server, instead of using a global redis server.
+  This improves compatibility with other services that use redis.
+
+  Redis database is used for storage only cache and job queue. More information can be found here - [Peertube architecture](https://docs.joinpeertube.org/contribute-architecture).
+
+  If you do want to save the redis database, you can use the following commands before upgrade OS:
+  ```bash
+  redis-cli save
+  sudo mkdir /var/lib/redis-peertube
+  sudo cp /var/lib/redis/dump.rdb /var/lib/redis-peertube/dump.rdb
+  ```
+
 - If you are using Wayland you can choose to use the Ozone Wayland support
   in Chrome and several Electron apps by setting the environment variable
   `NIXOS_OZONE_WL=1` (for example via
@@ -240,14 +787,29 @@ In addition to numerous new and upgraded packages, this release has the followin
   still under heavy development and behavior is not always flawless.
   Furthermore, not all Electron apps use the latest Electron versions.
 
+- A new option group `systemd.network.wait-online` was added, with options to configure `systemd-networkd-wait-online.service`:
+  - `anyInterface` allows specifying that the network should be considered online when *at least one* interface is online (useful on laptops)
+  - `timeout` defines how long to wait for the network to come online
+  - `extraArgs` for everything else
+
 - The `influxdb2` package was split into `influxdb2-server` and
   `influxdb2-cli`, matching the split that took place upstream. A
   combined `influxdb2` package is still provided in this release for
   backwards compatibilty, but will be removed at a later date.
 
+- The `unifi` package was switched from `unifi6` to `unifi7`.
+  Direct downgrades from Unifi 7 to Unifi 6 are not possible and require restoring from a backup made by Unifi 6.
+
+- `programs.zsh.autosuggestions.strategy` now takes a list of strings instead of a string.
+
+- The `asterisk` and `asterisk-stable` packages were switched from `asterisk_18` to the newly-packaged `asterisk_19`. Asterisk 13 and 17 have been removed as they have reached their end of life.
+
 - The `services.unifi.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
   Configurations using this default will print a warning when rebuilt.
 
+- The `services.unifi-video.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
+  Configurations using this default will print a warning when rebuilt.
+
 - `security.acme` certificates will now correctly check for CA
   revokation before reaching their minimum age.
 
@@ -266,49 +828,167 @@ In addition to numerous new and upgraded packages, this release has the followin
   e.g. Wayland.
 
 - [programs.ssh.knownHosts](#opt-programs.ssh.knownHosts) has gained an `extraHostNames`
-  option to replace `hostNames`. `hostNames` is deprecated, but still available for now.
+  option to augment `hostNames`. It is now possible to use the attribute name of a `knownHosts`
+  entry as the primary host name and specify secondary host names using `extraHostNames` without
+  having to duplicate the primary host name.
 
 - The `services.stubby` module was converted to a [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
 
+- The option
+  [services.xserver.desktopManager.runXdgAutostartIfNone](#opt-services.xserver.desktopManager.runXdgAutostartIfNone)
+  was added in order to automatically run XDG autostart files for sessions without a desktop manager.
+  This replaces helpers like the `dex` package.
+
+- When setting [i18n.inputMethod.enabled](#opt-i18n.inputMethod.enabled) to `fcitx5`,
+  it no longer creates corresponding systemd user services.
+  It now relies on XDG autostart files to start and work properly in your desktop sessions.
+  If you are using only a window manager without a desktop manager, you need to enable
+  `services.xserver.desktopManager.runXdgAutostartIfNone` or using the `dex` package to make `fcitx5` work.
+
+
 - The option `services.duplicati.dataDir` has been added to allow changing the location of duplicati's files.
 
-- A new option `boot.initrd.extraModprobeConfig` has been added which can be used to configure kernel modules that are loaded in the initrd.
+- The options `boot.extraModprobeConfig` and `boot.blacklistedKernelModules` now also take effect in the initrd by copying the file `/etc/modprobe.d/nixos.conf` into the initrd.
 
 - `nixos-generate-config` now puts the dhcp configuration in `hardware-configuration.nix` instead of `configuration.nix`.
 
+- ORY Kratos was updated to version 0.9.0-alpha.3, which introduces some breaking changes:
+  - All endpoints at the Admin API are now exposed at `/admin/`. For example, endpoint `https://kratos:4434/identities` is now exposed at `https://kratos:4434/admin/identities`
+  - Configuration key `selfservice.whitelisted_return_urls` has been renamed to `allowed_return_urls`
+  - The `password_identifier` form field of the password login strategy has been renamed to `identifier` to make compatibility with passwordless flows possible.
+  - Instead of having a global `default_schema_url` which developers used to update their schema, you now need to define the `default_schema_id` which must reference schema ID in your config.
+  - Calling `/self-service/recovery` without flow ID or with an invalid flow ID while authenticated will now respond with an error instead of redirecting to the default page.
+  - If you are relying on the SQLite images, update your Docker Pull commands as follows:
+    - `docker pull oryd/kratos:{version}`
+  - Additionally, all passwords now have to be at least 8 characters long.
+  - For more details, see:
+    - [Release Notes for v0.8.1-alpha-1](https://github.com/ory/kratos/releases/tag/v0.8.1-alpha.1)
+    - [Release Notes for v0.8.2-alpha-1](https://github.com/ory/kratos/releases/tag/v0.8.2-alpha.1)
+    - [Release Notes for v0.9.0-alpha-1](https://github.com/ory/kratos/releases/tag/v0.9.0-alpha.1)
+    - [Release Notes for v0.9.0-alpha-3](https://github.com/ory/kratos/releases/tag/v0.9.0-alpha.3)
+
+
 - `fetchFromSourcehut` now allows fetching repositories recursively
   using `fetchgit` or `fetchhg` if the argument `fetchSubmodules`
   is set to `true`.
 
+- A module for declarative configuration of openconnect VPN profiles was added under `networking.openconnect`.
+
+- The `element-desktop` package now has an `useKeytar` option (defaults to `true`),
+  which allows disabling `keytar` and in turn `libsecret` usage
+  (which binds to native credential managers / keychain libraries).
+
 - The option `services.thelounge.plugins` has been added to allow installing plugins for The Lounge. Plugins can be found in `pkgs.theLoungePlugins.plugins` and `pkgs.theLoungePlugins.themes`.
 
+- The option `services.xserver.videoDriver = [ "nvidia" ];` will now also install [nvidia VA-API drivers](https://github.com/elFarto/nvidia-vaapi-driver) by default.
+
 - The `firmwareLinuxNonfree` package has been renamed to `linux-firmware`.
 
+- It is now possible to specify wordlists to include as handy to access environment variables using the `config.environment.wordlist` configuration options.
+
 - The `services.mbpfan` module was converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
 
 - The default value for `programs.spacefm.settings.graphical_su` got unset. It previously pointed to `gksu` which has been removed.
 
-- A new module was added for the [Starship](https://starship.rs/) shell prompt,
-  providing the options `programs.starship.enable` and `programs.starship.settings`.
+- The [Dino](https://dino.im) XMPP client was updated to 0.3, adding support for audio and video calls.
 
 - `services.mattermost.plugins` has been added to allow the declarative installation of Mattermost plugins.
   Plugins are automatically repackaged using autoPatchelf.
 
+- [services.logrotate.enable](#opt-services.logrotate.enable) now defaults to true if any rotate path has
+  been defined, and some paths have been added by default.
+- The logrotate module also has been updated to freeform syntax: [services.logrotate.paths](#opt-services.logrotate.paths)
+  and [services.logrotate.extraConfig](#opt-services.logrotate.extraConfig) will work, but issue deprecation
+  warnings and [services.logrotate.settings](#opt-services.logrotate.settings) should now be used instead.
+
+- `security.pam.ussh` has been added, which allows authorizing PAM sessions based on SSH _certificates_ held within an SSH agent, using [pam-ussh](https://github.com/uber/pam-ussh).
+
+- The `vscode-extensions.ionide.ionide-fsharp` package has been updated to 6.0.0 and now requires .NET 6.0.
+
+- The `phpPackages.box` package has been updated from 2.7.5 to 3.16.0. See the [upgrade guide](https://github.com/box-project/box/blob/master/UPGRADE.md#from-27-to-30) for more details.
+
 - The `zrepl` package has been updated from 0.4.0 to 0.5:
 
   - The RPC protocol version was bumped; all zrepl daemons in a setup must be updated and restarted before replication can resume.
   - A bug involving encrypt-on-receive has been fixed. Read the [zrepl documentation](https://zrepl.github.io/configuration/sendrecvoptions.html#job-recv-options-placeholder) and check the output of `zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS` on the receiver.
 
+- The `polybar` package has been updated from 3.5.7 to 3.6.2. See [the changelog](https://github.com/polybar/polybar/releases/tag/3.6.0) for more details.
+  - Breaking changes include changes to escaping rules in configuration values, changes in behavior when encountering invalid tag names, and changes to inter-process-messaging (IPC).
+
 - Renamed option `services.openssh.challengeResponseAuthentication` to `services.openssh.kbdInteractiveAuthentication`.
   Reason is that the old name has been deprecated upstream.
   Using the old option name will still work, but produce a warning.
 
+- `services.autorandr` now allows for adding hooks and profiles declaratively.
+
+- The `pomerium-cli` command has been moved out of the `pomerium` package into
+  the `pomerium-cli` package, following upstream's repository split. If you are
+  using the `pomerium-cli` command, you should now install the `pomerium-cli`
+  package.
+
 - The option
-  [services.networking.networkmanager.enableFccUnlock](#opt-services.networking.networkmanager.enableFccUnlock)
+  [services.networking.networkmanager.enableFccUnlock](#opt-networking.networkmanager.enableFccUnlock)
   was added to support FCC unlock procedures. Since release 1.18.4, the ModemManager
   daemon no longer automatically performs the FCC unlock procedure by default. See
   [the docs](https://modemmanager.org/docs/modemmanager/fcc-unlock/) for more details.
 
 - `programs.tmux` has a new option `plugins` that accepts a list of packages from the `tmuxPlugins` group. The specified packages are added to the system and loaded by `tmux`.
 
+- The polkit service, available at `security.polkit.enable`, is now disabled by default. It will automatically be enabled through services and desktop environments as needed.
+
+- `mercury` was updated to 22.01.1, which has some breaking changes ([Mercury 22.01 news](https://dl.mercurylang.org/release/release-notes-22.01.html)).
+
+- xfsprogs was update to version 5.15, which enables inobtcount and bigtime by default on filesystem creation. Support for these features was added in kernel 5.10 and deemed stable in kernel 5.15.
+  If you want to be able to mount XFS filesystems created with this release of xfsprogs on kernel releases older than 5.10, you need to format them with `mkfs.xfs -m bigtime=0 -m inobtcount=0`.
+
+- `services.xserver.desktopManager.xfce` now includes Xfce's screen locker, `xfce4-screensaver` that is enabled by default. You can disable it by setting `false` to [services.xserver.desktopManager.xfce.enableScreensaver](#opt-services.xserver.desktopManager.xfce.enableScreensaver).
+
+- The `hadoop` package has added support for `aarch64-linux` and `aarch64-darwin` as of 3.3.1 ([#158613](https://github.com/NixOS/nixpkgs/pull/158613)).
+
+- The `R` package now builds again on `aarch64-darwin` ([#158992](https://github.com/NixOS/nixpkgs/pull/158992)).
+
+- The `nss` package was split into `nss_esr` and `nss_latest`, with `nss` being an alias for `nss_esr`. This was done to ease maintenance of `nss` and dependent high-profile packages like `firefox`.
+
+- The default `scribus` version is now 1.5, while version 1.4 is still available as `scribus_1_4` ([#172700](https://github.com/NixOS/nixpkgs/pull/172700)).
+
+- The Nextcloud module now supports to create a Mysql database automatically
+  with `services.nextcloud.database.createLocally` enabled.
+
+- The Nextcloud module now allows setting the value of the `max-age` directive of the `Strict-Transport-Security` HTTP header, which is now controlled by the `services.nextcloud.https` option, rather than `services.nginx.recommendedHttpHeaders`.
+
+- The `spark3` package has been updated from 3.1.2 to 3.2.1 ([#160075](https://github.com/NixOS/nixpkgs/pull/160075)):
+
+  - Testing has been enabled for `aarch64-linux` in addition to `x86_64-linux`.
+  - The `spark3` package is now usable on `aarch64-darwin` as a result of [#158613](https://github.com/NixOS/nixpkgs/pull/158613) and [#158992](https://github.com/NixOS/nixpkgs/pull/158992).
+
+- The option `services.snapserver.openFirewall` will no longer default to
+  `true` starting with NixOS 22.11. Enable it explicitly if you need to control
+  Snapserver remotely or connect streamig clients from other hosts.
+
+- The option [networking.useDHCP](options.html#opt-networking.useDHCP) isn't deprecated anymore.
+  When using [`systemd-networkd`](options.html#opt-networking.useNetworkd), a generic
+  `.network`-unit is added which enables DHCP for each interface matching `en*`, `eth*`
+  or `wl*` with priority 99 (which means that it doesn't have any effect if such an interface is matched
+  by a `.network-`unit with a lower priority). In case of scripted networking, no behavior
+  was changed.
+  
+- The new [`postgresqlTestHook`](https://nixos.org/manual/nixpkgs/stable/#sec-postgresqlTestHook) runs a PostgreSQL server for the duration of package checks.
+
+- `zfs` was updated from 2.1.4 to 2.1.5, enabling it to be used with Linux kernel 5.18.
+
+- `stdenv.mkDerivation` now supports a self-referencing `finalAttrs:` parameter
+  containing the final `mkDerivation` arguments including overrides.
+  `drv.overrideAttrs` now supports two parameters `finalAttrs: previousAttrs:`.
+  This allows packaging configuration to be overridden in a consistent manner by
+  providing an alternative to `rec {}` syntax.
+
+  Additionally, `passthru` can now reference `finalAttrs.finalPackage` containing
+  the final package, including attributes such as the output paths and
+  `overrideAttrs`.
+
+  New language integrations can be simplified by overriding a "prototype"
+  package containing the language-specific logic. This removes the need for a
+  extra layer of overriding for the "generic builder" arguments, thus removing a
+  usability problem and source of error.
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
new file mode 100644
index 0000000000000..3f9afe13f1d99
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -0,0 +1,139 @@
+# Release 22.11 (“Raccoon”, 2022.11/??) {#sec-release-22.11}
+
+Support is planned until the end of June 2023, handing over to 23.05.
+
+## Highlights {#sec-release-22.11-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- During cross-compilation, tests are now executed if the test suite can be executed
+  by the build platform. This is the case when doing “native” cross-compilation
+  where the build and host platforms are largely the same, but the nixpkgs' cross
+  compilation infrastructure is used, e.g. `pkgsStatic` and `pkgsLLVM`. Another
+  possibility is that the build platform is a superset of the host platform, e.g. when
+  cross-compiling from `x86_64-unknown-linux` to `i686-unknown-linux`.
+  The predicate gating test suite execution is the newly added `canExecute`
+  predicate: You can e.g. check if `stdenv.buildPlatform` can execute binaries
+  built for `stdenv.hostPlatform` (i.e. produced by `stdenv.cc`) by evaluating
+  `stdenv.buildPlatform.canExecute stdenv.hostPlatform`.
+
+- The `nixpkgs.hostPlatform` and `nixpkgs.buildPlatform` options have been added.
+  These cover and override the `nixpkgs.{system,localSystem,crossSystem}` options.
+
+   - `hostPlatform` is the platform or "`system`" string of the NixOS system
+     described by the configuration.
+   - `buildPlatform` is the platform that is responsible for building the NixOS
+     configuration. It defaults to the `hostPlatform`, for a non-cross
+     build configuration. To cross compile, set `buildPlatform` to a different
+     value.
+
+  The new options convey the same information, but with fewer options, and
+  following the Nixpkgs terminology.
+
+  The existing options `nixpkgs.{system,localSystem,crossSystem}` have not
+  been formally deprecated, to allow for evaluation of the change and to allow
+  for a transition period so that in time the ecosystem can switch without
+  breaking compatibility with any supported NixOS release.
+
+- `nixos-generate-config` now generates configurations that can be built in pure
+  mode. This is achieved by setting the new `nixpkgs.hostPlatform` option.
+
+  You may have to unset the `system` parameter in `lib.nixosSystem`, or similarly
+  remove definitions of the `nixpkgs.{system,localSystem,crossSystem}` options.
+
+  Alternatively, you can remove the `hostPlatform` line and use NixOS like you
+  would in NixOS 22.05 and earlier.
+
+- PHP now defaults to PHP 8.1, updated from 8.0.
+
+- `hardware.nvidia` has a new option `open` that can be used to opt in the opensource version of NVIDIA kernel driver. Note that the driver's support for GeForce and Workstation GPUs is still alpha quality, see [NVIDIA Releases Open-Source GPU Kernel Modules](https://developer.nvidia.com/blog/nvidia-releases-open-source-gpu-kernel-modules/) for the official announcement.
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
+## New Services {#sec-release-22.11-new-services}
+
+- [appvm](https://github.com/jollheef/appvm), Nix based app VMs. Available as [virtualisation.appvm](options.html#opt-virtualisation.appvm.enable).
+
+- [dragonflydb](https://dragonflydb.io/), a modern replacement for Redis and Memcached. Available as [services.dragonflydb](#opt-services.dragonflydb.enable).
+
+- [infnoise](https://github.com/leetronics/infnoise), a hardware True Random Number Generator dongle.
+  Available as [services.infnoise](options.html#opt-services.infnoise.enable).
+- [persistent-evdev](https://github.com/aiberia/persistent-evdev), a daemon to add virtual proxy devices that mirror a physical input device but persist even if the underlying hardware is hot-plugged. Available as [services.persistent-evdev](#opt-services.persistent-evdev.enable).
+
+- [schleuder](https://schleuder.org/), a mailing list manager with PGP support. Enable using [services.schleuder](#opt-services.schleuder.enable).
+
+- [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable).
+
+- [Grafana Tempo](https://www.grafana.com/oss/tempo/), a distributed tracing store. Available as [services.tempo](#opt-services.tempo.enable).
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
+## Backward Incompatibilities {#sec-release-22.11-incompatibilities}
+
+- The `isCompatible` predicate checking CPU compatibility is no longer exposed
+  by the platform sets generated using `lib.systems.elaborate`. In most cases
+  you will want to use the new `canExecute` predicate instead which also
+  considers the kernel / syscall interface. It is briefly described in the
+  release's [highlights section](#sec-release-22.11-highlights).
+  `lib.systems.parse.isCompatible` still exists, but has changed semantically:
+  Architectures with differing endianness modes are *no longer considered compatible*.
+
+- `ngrok` has been upgraded from 2.3.40 to 3.0.4. Please see [the upgrade guide](https://ngrok.com/docs/guides/upgrade-v2-v3)
+  and [changelog](https://ngrok.com/docs/ngrok-agent/changelog). Notably, breaking changes are that the config file format has
+  changed and support for single hypen arguments was dropped.
+
+- `i18n.supportedLocales` is now by default only generated with the locales set in `i18n.defaultLocale` and `i18n.extraLocaleSettings`.
+  This got partially copied over from the minimal profile and reduces the final system size by up to 200MB.
+  If you require all locales installed set the option to ``[ "all" ]``.
+
+- The `isPowerPC` predicate, found on `platform` attrsets (`hostPlatform`, `buildPlatform`, `targetPlatform`, etc) has been removed in order to reduce confusion.  The predicate was was defined such that it matches only the 32-bit big-endian members of the POWER/PowerPC family, despite having a name which would imply a broader set of systems.  If you were using this predicate, you can replace `foo.isPowerPC` with `(with foo; isPower && is32bit && isBigEndian)`.
+
+- `bsp-layout` no longer uses the command `cycle` to switch to other window layouts, as it got replaced by the commands `previous` and `next`.
+
+- The Barco ClickShare driver/client package `pkgs.clickshare-csc1` and the option `programs.clickshare-csc1.enable` have been removed,
+  as it requires `qt4`, which reached its end-of-life 2015 and will no longer be supported by nixpkgs.
+  [According to Barco](https://www.barco.com/de/support/knowledge-base/4380-can-i-use-linux-os-with-clickshare-base-units) many of their base unit models can be used with Google Chrome and the Google Cast extension.
+
+- PHP 7.4 is no longer supported due to upstream not supporting this
+  version for the entire lifecycle of the 22.11 release.
+
+- riak package removed along with `services.riak` module, due to lack of maintainer to update the package.
+
+- The `services.graphite.api` and `services.graphite.beacon` NixOS options, and
+  the `python3.pkgs.graphite_api`, `python3.pkgs.graphite_beacon` and
+  `python3.pkgs.influxgraph` packages, have been removed due to lack of upstream
+  maintenance.
+
+- The `meta.mainProgram` attribute of packages in `wineWowPackages` now defaults to `"wine64"`.
+
+- (Neo)Vim can not be configured with `configure.pathogen` anymore to reduce maintainance burden.
+Use `configure.packages` instead.
+
+- `k3s` no longer supports docker as runtime due to upstream dropping support.
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
+## Other Notable Changes {#sec-release-22.11-notable-changes}
+
+- The `xplr` package has been updated from 0.18.0 to 0.19.0, which brings some breaking changes. See the [upstream release notes](https://github.com/sayanarijit/xplr/releases/tag/v0.19.0) for more details.
+
+- A new module was added for the Saleae Logic device family, providing the options `hardware.saleae-logic.enable` and `hardware.saleae-logic.package`.
+
+- The Redis module now disables RDB persistence when `services.redis.servers.<name>.save = []` instead of using the Redis default.
+
+- Matrix Synapse now requires entries in the `state_group_edges` table to be unique, in order to prevent accidentally introducing duplicate information (for example, because a database backup was restored multiple times). If your Synapse database already has duplicate rows in this table, this could fail with an error and require manual remediation.
+
+- `dockerTools.buildImage` deprecates the misunderstood `contents` parameter, in favor of `copyToRoot`.
+  Use `copyToRoot = buildEnv { ... };` or similar if you intend to add packages to `/bin`.
+
+- memtest86+ was updated from 5.00-coreboot-002 to 6.00-beta2. It is now the upstream version from https://www.memtest.org/, as coreboot's fork is no longer available.
+
+- Add udev rules for the Teensy family of microcontrollers.
+
+- The `pass-secret-service` package now includes systemd units from upstream, so adding it to the NixOS `services.dbus.packages` option will make it start automatically as a systemd user service when an application tries to talk to the libsecret D-Bus API.
+
+- There is a new module for the `thunar` program (the Xfce file manager), which depends on the `xfconf` dbus service, and also has a dbus service and a systemd unit. The option `services.xserver.desktopManager.xfce.thunarPlugins` has been renamed to `programs.thunar.plugins`, and in a future release it may be removed.
+
+- There is a new module for the `xfconf` program (the Xfce configuration storage system), which has a dbus service.
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/lib/build-vms.nix b/nixos/lib/build-vms.nix
index 05d9ce89dbdc3..18af49db17777 100644
--- a/nixos/lib/build-vms.nix
+++ b/nixos/lib/build-vms.nix
@@ -38,7 +38,7 @@ rec {
           { key = "no-revision";
             # Make the revision metadata constant, in order to avoid needless retesting.
             # The human version (e.g. 21.05-pre) is left as is, because it is useful
-            # for external modules that test with e.g. nixosTest and rely on that
+            # for external modules that test with e.g. testers.nixosTest and rely on that
             # version number.
             config.system.nixos.revision = mkForce "constant-nixos-revision";
           }
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
index 2daaa8a118632..3b58ef297973e 100644
--- a/nixos/lib/eval-config.nix
+++ b/nixos/lib/eval-config.nix
@@ -50,11 +50,6 @@ let
       # they way through, but has the last priority behind everything else.
       nixpkgs.system = lib.mkDefault system;
 
-      # Stash the value of the `system` argument. When using `nesting.children`
-      # we want to have the same default value behavior (immediately above)
-      # without any interference from the user's configuration.
-      nixpkgs.initialSystem = system;
-
       _module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_);
     };
   };
diff --git a/nixos/lib/make-disk-image.nix b/nixos/lib/make-disk-image.nix
index 15302ae824144..e784ec9e67787 100644
--- a/nixos/lib/make-disk-image.nix
+++ b/nixos/lib/make-disk-image.nix
@@ -170,6 +170,7 @@ let format' = format; in let
       config.system.build.nixos-install
       config.system.build.nixos-enter
       nix
+      systemdMinimal
     ] ++ stdenv.initialPath);
 
   # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
diff --git a/nixos/lib/make-ext4-fs.nix b/nixos/lib/make-ext4-fs.nix
index 416beeb32f2fa..b8e1b8d24c48b 100644
--- a/nixos/lib/make-ext4-fs.nix
+++ b/nixos/lib/make-ext4-fs.nix
@@ -78,6 +78,15 @@ pkgs.stdenv.mkDerivation {
       # get rid of the unnecessary slack here--but see
       # https://github.com/NixOS/nixpkgs/issues/125121 for caveats.
 
+      # shrink to fit
+      resize2fs -M $img
+
+      # Add 16 MebiByte to the current_size
+      new_size=$(dumpe2fs -h $img | awk -F: \
+        '/Block count/{count=$2} /Block size/{size=$2} END{print (count*size+16*2**20)/size}')
+
+      resize2fs $img $new_size
+
       if [ ${builtins.toString compressImage} ]; then
         echo "Compressing image"
         zstd -v --no-progress ./$img -o $out
diff --git a/nixos/lib/make-zfs-image.nix b/nixos/lib/make-multi-disk-zfs-image.nix
index a84732aa11712..0a894c8b9888f 100644
--- a/nixos/lib/make-zfs-image.nix
+++ b/nixos/lib/make-multi-disk-zfs-image.nix
@@ -128,7 +128,7 @@ let
       gptfdisk
       nix
       parted
-      utillinux
+      util-linux
       zfs
     ]
   );
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index 57652dd5db1e7..6649fc41d41aa 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -20,6 +20,12 @@
 , lib
 , options
 , transformOptions ? lib.id  # function for additional tranformations of the options
+, documentType ? "appendix" # TODO deprecate "appendix" in favor of "none"
+                            #      and/or rename function to moduleOptionDoc for clean slate
+
+  # If you include more than one option list into a document, you need to
+  # provide different ids.
+, variablelistId ? "configuration-variable-list"
 , revision ? "" # Specify revision for the options
 # a set of options the docs we are generating will be merged into, as if by recursiveUpdate.
 # used to split the options doc build into a static part (nixos/modules) and a dynamic part
@@ -45,8 +51,11 @@ let
     else if lib.isFunction x then "<function>"
     else x;
 
-  optionsList = lib.flip map optionsListVisible
-   (opt: transformOptions opt
+  rawOpts = lib.optionAttrSetToDocList options;
+  transformedOpts = map transformOptions rawOpts;
+  filteredOpts = lib.filter (opt: opt.visible && !opt.internal) transformedOpts;
+  optionsList = lib.flip map filteredOpts
+   (opt: opt
     // lib.optionalAttrs (opt ? example) { example = substSpecial opt.example; }
     // lib.optionalAttrs (opt ? default) { default = substSpecial opt.default; }
     // lib.optionalAttrs (opt ? type) { type = substSpecial opt.type; }
@@ -88,9 +97,6 @@ let
         '';
     in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
 
-  # Remove invisible and internal options.
-  optionsListVisible = lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList options);
-
   optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
 
 in rec {
@@ -110,7 +116,15 @@ in rec {
 
   optionsJSON = pkgs.runCommand "options.json"
     { meta.description = "List of NixOS options in JSON format";
-      buildInputs = [ pkgs.brotli ];
+      buildInputs = [
+        pkgs.brotli
+        (let
+          self = (pkgs.python3Minimal.override {
+            inherit self;
+            includeSiteCustomize = true;
+           });
+         in self.withPackages (p: [ p.mistune_2_0 ]))
+      ];
       options = builtins.toFile "options.json"
         (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix));
     }
@@ -121,9 +135,13 @@ in rec {
 
       ${
         if baseOptionsJSON == null
-          then "cp $options $dst/options.json"
+          then ''
+            # `cp $options $dst/options.json`, but with temporary
+            # markdown processing
+            python ${./mergeJSON.py} $options <(echo '{}') > $dst/options.json
+          ''
           else ''
-            ${pkgs.python3Minimal}/bin/python ${./mergeJSON.py} \
+            python ${./mergeJSON.py} \
               ${lib.optionalString warningsAreErrors "--warnings-are-errors"} \
               ${baseOptionsJSON} $options \
               > $dst/options.json
@@ -161,7 +179,9 @@ in rec {
 
     ${pkgs.python3Minimal}/bin/python ${./sortXML.py} $optionsXML sorted.xml
     ${pkgs.libxslt.bin}/bin/xsltproc \
+      --stringparam documentType '${documentType}' \
       --stringparam revision '${revision}' \
+      --stringparam variablelistId '${variablelistId}' \
       -o intermediate.xml ${./options-to-docbook.xsl} sorted.xml
     ${pkgs.libxslt.bin}/bin/xsltproc \
       -o "$out" ${./postprocess-option-descriptions.xsl} intermediate.xml
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
index 029787a31586c..33e5172270b58 100644
--- a/nixos/lib/make-options-doc/mergeJSON.py
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -41,6 +41,154 @@ def unpivot(options: Dict[Key, Option]) -> Dict[str, JSON]:
         result[opt.name] = opt.value
     return result
 
+# converts in-place!
+def convertMD(options: Dict[str, Any]) -> str:
+    import mistune
+    import re
+    from xml.sax.saxutils import escape, quoteattr
+
+    admonitions = {
+        '.warning': 'warning',
+        '.important': 'important',
+        '.note': 'note'
+    }
+    class Renderer(mistune.renderers.BaseRenderer):
+        def _get_method(self, name):
+            try:
+                return super(Renderer, self)._get_method(name)
+            except AttributeError:
+                def not_supported(*args, **kwargs):
+                    raise NotImplementedError("md node not supported yet", name, args, **kwargs)
+                return not_supported
+
+        def text(self, text):
+            return escape(text)
+        def paragraph(self, text):
+            return text + "\n\n"
+        def newline(self):
+            return "<literallayout>\n</literallayout>"
+        def codespan(self, text):
+            return f"<literal>{escape(text)}</literal>"
+        def block_code(self, text, info=None):
+            info = f" language={quoteattr(info)}" if info is not None else ""
+            return f"<programlisting{info}>\n{escape(text)}</programlisting>"
+        def link(self, link, text=None, title=None):
+            if link[0:1] == '#':
+                attr = "linkend"
+                link = quoteattr(link[1:])
+            else:
+                # try to faithfully reproduce links that were of the form <link href="..."/>
+                # in docbook format
+                if text == link:
+                    text = ""
+                attr = "xlink:href"
+                link = quoteattr(link)
+            return f"<link {attr}={link}>{text}</link>"
+        def list(self, text, ordered, level, start=None):
+            if ordered:
+                raise NotImplementedError("ordered lists not supported yet")
+            return f"<itemizedlist>\n{text}\n</itemizedlist>"
+        def list_item(self, text, level):
+            return f"<listitem><para>{text}</para></listitem>\n"
+        def block_text(self, text):
+            return text
+        def emphasis(self, text):
+            return f"<emphasis>{text}</emphasis>"
+        def strong(self, text):
+            return f"<emphasis role=\"strong\">{text}</emphasis>"
+        def admonition(self, text, kind):
+            if kind not in admonitions:
+                raise NotImplementedError(f"admonition {kind} not supported yet")
+            tag = admonitions[kind]
+            # we don't keep whitespace here because usually we'll contain only
+            # a single paragraph and the original docbook string is no longer
+            # available to restore the trailer.
+            return f"<{tag}><para>{text.rstrip()}</para></{tag}>"
+        def block_quote(self, text):
+            return f"<blockquote><para>{text}</para></blockquote>"
+        def command(self, text):
+            return f"<command>{escape(text)}</command>"
+        def option(self, text):
+            return f"<option>{escape(text)}</option>"
+        def file(self, text):
+            return f"<filename>{escape(text)}</filename>"
+        def manpage(self, page, section):
+            title = f"<refentrytitle>{escape(page)}</refentrytitle>"
+            vol = f"<manvolnum>{escape(section)}</manvolnum>"
+            return f"<citerefentry>{title}{vol}</citerefentry>"
+
+        def finalize(self, data):
+            return "".join(data)
+
+    plugins = []
+
+    COMMAND_PATTERN = r'\{command\}`(.*?)`'
+    def command(md):
+        def parse(self, m, state):
+            return ('command', m.group(1))
+        md.inline.register_rule('command', COMMAND_PATTERN, parse)
+        md.inline.rules.append('command')
+    plugins.append(command)
+
+    FILE_PATTERN = r'\{file\}`(.*?)`'
+    def file(md):
+        def parse(self, m, state):
+            return ('file', m.group(1))
+        md.inline.register_rule('file', FILE_PATTERN, parse)
+        md.inline.rules.append('file')
+    plugins.append(file)
+
+    OPTION_PATTERN = r'\{option\}`(.*?)`'
+    def option(md):
+        def parse(self, m, state):
+            return ('option', m.group(1))
+        md.inline.register_rule('option', OPTION_PATTERN, parse)
+        md.inline.rules.append('option')
+    plugins.append(option)
+
+    MANPAGE_PATTERN = r'\{manpage\}`(.*?)\((.+?)\)`'
+    def manpage(md):
+        def parse(self, m, state):
+            return ('manpage', m.group(1), m.group(2))
+        md.inline.register_rule('manpage', MANPAGE_PATTERN, parse)
+        md.inline.rules.append('manpage')
+    plugins.append(manpage)
+
+    ADMONITION_PATTERN = re.compile(r'^::: \{([^\n]*?)\}\n(.*?)^:::\n', flags=re.MULTILINE|re.DOTALL)
+    def admonition(md):
+        def parse(self, m, state):
+            return {
+                'type': 'admonition',
+                'children': self.parse(m.group(2), state),
+                'params': [ m.group(1) ],
+            }
+        md.block.register_rule('admonition', ADMONITION_PATTERN, parse)
+        md.block.rules.append('admonition')
+    plugins.append(admonition)
+
+    def convertString(text: str) -> str:
+        rendered = mistune.markdown(text, renderer=Renderer(), plugins=plugins)
+        # keep trailing spaces so we can diff the generated XML to check for conversion bugs.
+        return rendered.rstrip() + text[len(text.rstrip()):]
+
+    def optionIs(option: Dict[str, Any], key: str, typ: str) -> bool:
+        if key not in option: return False
+        if type(option[key]) != dict: return False
+        if '_type' not in option[key]: return False
+        return option[key]['_type'] == typ
+
+    for (name, option) in options.items():
+        if optionIs(option, 'description', 'mdDoc'):
+            option['description'] = convertString(option['description']['text'])
+        if optionIs(option, 'example', 'literalMD'):
+            docbook = convertString(option['example']['text'])
+            option['example'] = { '_type': 'literalDocBook', 'text': docbook }
+        if optionIs(option, 'default', 'literalMD'):
+            docbook = convertString(option['default']['text'])
+            option['default'] = { '_type': 'literalDocBook', 'text': docbook }
+
+    return options
+
 warningsAreErrors = sys.argv[1] == "--warnings-are-errors"
 optOffset = 1 if warningsAreErrors else 0
 options = pivot(json.load(open(sys.argv[1 + optOffset], 'r')))
@@ -48,7 +196,9 @@ overrides = pivot(json.load(open(sys.argv[2 + optOffset], 'r')))
 
 # fix up declaration paths in lazy options, since we don't eval them from a full nixpkgs dir
 for (k, v) in options.items():
-    v.value['declarations'] = list(map(lambda s: f'nixos/modules/{s}', v.value['declarations']))
+    # The _module options are not declared in nixos/modules
+    if v.value['loc'][0] != "_module":
+        v.value['declarations'] = list(map(lambda s: f'nixos/modules/{s}' if isinstance(s, str) else s, v.value['declarations']))
 
 # merge both descriptions
 for (k, v) in overrides.items():
@@ -66,14 +216,21 @@ for (k, v) in overrides.items():
         elif ov is not None or cur.get(ok, None) is None:
             cur[ok] = ov
 
+severity = "error" if warningsAreErrors else "warning"
+
 # check that every option has a description
 hasWarnings = False
 for (k, v) in options.items():
     if v.value.get('description', None) is None:
-        severity = "error" if warningsAreErrors else "warning"
         hasWarnings = True
         print(f"\x1b[1;31m{severity}: option {v.name} has no description\x1b[0m", file=sys.stderr)
         v.value['description'] = "This option has no description."
+    if v.value.get('type', "unspecified") == "unspecified":
+        hasWarnings = True
+        print(
+            f"\x1b[1;31m{severity}: option {v.name} has no type. Please specify a valid type, see " +
+            "https://nixos.org/manual/nixos/stable/index.html#sec-option-types\x1b[0m", file=sys.stderr)
+
 if hasWarnings and warningsAreErrors:
     print(
         "\x1b[1;31m" +
@@ -83,4 +240,4 @@ if hasWarnings and warningsAreErrors:
         file=sys.stderr)
     sys.exit(1)
 
-json.dump(unpivot(options), fp=sys.stdout)
+json.dump(convertMD(unpivot(options)), fp=sys.stdout)
diff --git a/nixos/lib/make-options-doc/options-to-docbook.xsl b/nixos/lib/make-options-doc/options-to-docbook.xsl
index b286f7b5e2c0a..978d5e2468a83 100644
--- a/nixos/lib/make-options-doc/options-to-docbook.xsl
+++ b/nixos/lib/make-options-doc/options-to-docbook.xsl
@@ -12,15 +12,36 @@
   <xsl:output method='xml' encoding="UTF-8" />
 
   <xsl:param name="revision" />
+  <xsl:param name="documentType" />
   <xsl:param name="program" />
+  <xsl:param name="variablelistId" />
 
 
   <xsl:template match="/expr/list">
-    <appendix xml:id="appendix-configuration-options">
-      <title>Configuration Options</title>
-      <variablelist xml:id="configuration-variable-list">
+    <xsl:choose>
+      <xsl:when test="$documentType = 'appendix'">
+        <appendix xml:id="appendix-configuration-options">
+          <title>Configuration Options</title>
+          <xsl:call-template name="variable-list"/>
+        </appendix>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:call-template name="variable-list"/>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+  <xsl:template name="variable-list">
+      <variablelist>
+      <xsl:attribute name="id" namespace="http://www.w3.org/XML/1998/namespace"><xsl:value-of select="$variablelistId"/></xsl:attribute>
         <xsl:for-each select="attrs">
-          <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '&lt;', '_'), '>', '_'), ':', '_'))" />
+          <xsl:variable name="id" select="
+            concat('opt-',
+              translate(
+                attr[@name = 'name']/string/@value,
+                '*&lt; >[]:',
+                '_______'
+            ))" />
           <varlistentry>
             <term xlink:href="#{$id}">
               <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute>
@@ -96,7 +117,6 @@
         </xsl:for-each>
 
       </variablelist>
-    </appendix>
   </xsl:template>
 
 
@@ -195,6 +215,23 @@
 
   <xsl:template match="attr[@name = 'declarations' or @name = 'definitions']">
     <simplelist>
+      <!--
+        Example:
+          opt.declarations = [ { name = "foo/bar.nix"; url = "https://github.com/....."; } ];
+      -->
+      <xsl:for-each select="list/attrs[attr[@name = 'name']]">
+        <member><filename>
+          <xsl:if test="attr[@name = 'url']">
+            <xsl:attribute name="xlink:href"><xsl:value-of select="attr[@name = 'url']/string/@value"/></xsl:attribute>
+          </xsl:if>
+          <xsl:value-of select="attr[@name = 'name']/string/@value"/>
+        </filename></member>
+      </xsl:for-each>
+
+      <!--
+        When the declarations/definitions are raw strings,
+        fall back to hardcoded location logic, specific to Nixpkgs.
+      -->
       <xsl:for-each select="list/string">
         <member><filename>
           <!-- Hyperlink the filename either to the NixOS Subversion
diff --git a/nixos/lib/make-single-disk-zfs-image.nix b/nixos/lib/make-single-disk-zfs-image.nix
new file mode 100644
index 0000000000000..98150584fb3e4
--- /dev/null
+++ b/nixos/lib/make-single-disk-zfs-image.nix
@@ -0,0 +1,322 @@
+# Note: This is a private API, internal to NixOS. Its interface is subject
+# to change without notice.
+#
+# The result of this builder is a single disk image, partitioned like this:
+#
+#  * partition #1: a very small, 1MiB partition to leave room for Grub.
+#
+#  * partition #2: boot, a partition formatted with FAT to be used for /boot.
+#      FAT is chosen to support EFI.
+#
+#  * partition #3: nixos, a partition dedicated to a zpool.
+#
+# This single-disk approach does not satisfy ZFS's requirements for autoexpand,
+# however automation can expand it anyway. For example, with
+# `services.zfs.expandOnBoot`.
+{ lib
+, pkgs
+, # The NixOS configuration to be installed onto the disk image.
+  config
+
+, # size of the FAT partition, in megabytes.
+  bootSize ? 1024
+
+, # The size of the root partition, in megabytes.
+  rootSize ? 2048
+
+, # The name of the ZFS pool
+  rootPoolName ? "tank"
+
+, # zpool properties
+  rootPoolProperties ? {
+    autoexpand = "on";
+  }
+, # pool-wide filesystem properties
+  rootPoolFilesystemProperties ? {
+    acltype = "posixacl";
+    atime = "off";
+    compression = "on";
+    mountpoint = "legacy";
+    xattr = "sa";
+  }
+
+, # datasets, with per-attribute options:
+  # mount: (optional) mount point in the VM
+  # properties: (optional) ZFS properties on the dataset, like filesystemProperties
+  # Notes:
+  # 1. datasets will be created from shorter to longer names as a simple topo-sort
+  # 2. you should define a root's dataset's mount for `/`
+  datasets ? { }
+
+, # The files and directories to be placed in the target file system.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents ? [ ]
+
+, # The initial NixOS configuration file to be copied to
+  # /etc/nixos/configuration.nix. This configuration will be embedded
+  # inside a configuration which includes the described ZFS fileSystems.
+  configFile ? null
+
+, # Shell code executed after the VM has finished.
+  postVM ? ""
+
+, name ? "nixos-disk-image"
+
+, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
+  format ? "raw"
+
+, # Include a copy of Nixpkgs in the disk image
+  includeChannel ? true
+}:
+let
+  formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
+
+  compress = lib.optionalString (format == "qcow2-compressed") "-c";
+
+  filenameSuffix = "." + {
+    qcow2 = "qcow2";
+    vdi = "vdi";
+    vpc = "vhd";
+    raw = "img";
+  }.${formatOpt} or formatOpt;
+  rootFilename = "nixos.root${filenameSuffix}";
+
+  # FIXME: merge with channel.nix / make-channel.nix.
+  channelSources =
+    let
+      nixpkgs = lib.cleanSource pkgs.path;
+    in
+    pkgs.runCommand "nixos-${config.system.nixos.version}" { } ''
+      mkdir -p $out
+      cp -prd ${nixpkgs.outPath} $out/nixos
+      chmod -R u+w $out/nixos
+      if [ ! -e $out/nixos/nixpkgs ]; then
+        ln -s . $out/nixos/nixpkgs
+      fi
+      rm -rf $out/nixos/.git
+      echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
+    '';
+
+  closureInfo = pkgs.closureInfo {
+    rootPaths = [ config.system.build.toplevel ]
+      ++ (lib.optional includeChannel channelSources);
+  };
+
+  modulesTree = pkgs.aggregateModules
+    (with config.boot.kernelPackages; [ kernel zfs ]);
+
+  tools = lib.makeBinPath (
+    with pkgs; [
+      config.system.build.nixos-enter
+      config.system.build.nixos-install
+      dosfstools
+      e2fsprogs
+      gptfdisk
+      nix
+      parted
+      util-linux
+      zfs
+    ]
+  );
+
+  hasDefinedMount = disk: ((disk.mount or null) != null);
+
+  stringifyProperties = prefix: properties: lib.concatStringsSep " \\\n" (
+    lib.mapAttrsToList
+      (
+        property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
+      )
+      properties
+  );
+
+  featuresToProperties = features:
+    lib.listToAttrs
+      (builtins.map
+        (feature: {
+          name = "feature@${feature}";
+          value = "enabled";
+        })
+        features);
+
+  createDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      sorted = lib.sort (left: right: (lib.stringLength left.name) < (lib.stringLength right.name)) datasetlist;
+      cmd = { name, value }:
+        let
+          properties = stringifyProperties "-o" (value.properties or { });
+        in
+        "zfs create -p ${properties} ${name}";
+    in
+    lib.concatMapStringsSep "\n" cmd sorted;
+
+  mountDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
+      sorted = lib.sort (left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)) mounts;
+      cmd = { name, value }:
+        ''
+          mkdir -p /mnt${lib.escapeShellArg value.mount}
+          mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
+        '';
+    in
+    lib.concatMapStringsSep "\n" cmd sorted;
+
+  unmountDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
+      sorted = lib.sort (left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)) mounts;
+      cmd = { name, value }:
+        ''
+          umount /mnt${lib.escapeShellArg value.mount}
+        '';
+    in
+    lib.concatMapStringsSep "\n" cmd sorted;
+
+
+  fileSystemsCfgFile =
+    let
+      mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
+    in
+    pkgs.runCommand "filesystem-config.nix"
+      {
+        buildInputs = with pkgs; [ jq nixpkgs-fmt ];
+        filesystems = builtins.toJSON {
+          fileSystems = lib.mapAttrs'
+            (
+              dataset: attrs:
+                {
+                  name = attrs.mount;
+                  value = {
+                    fsType = "zfs";
+                    device = "${dataset}";
+                  };
+                }
+            )
+            mountable;
+        };
+        passAsFile = [ "filesystems" ];
+      } ''
+      (
+        echo "builtins.fromJSON '''"
+        jq . < "$filesystemsPath"
+        echo "'''"
+      ) > $out
+
+      nixpkgs-fmt $out
+    '';
+
+  mergedConfig =
+    if configFile == null
+    then fileSystemsCfgFile
+    else
+      pkgs.runCommand "configuration.nix"
+        {
+          buildInputs = with pkgs; [ nixpkgs-fmt ];
+        }
+        ''
+          (
+            echo '{ imports = ['
+            printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
+            printf "(%s)\n" "$(cat ${configFile})";
+            echo ']; }'
+          ) > $out
+
+          nixpkgs-fmt $out
+        '';
+
+  image = (
+    pkgs.vmTools.override {
+      rootModules =
+        [ "zfs" "9p" "9pnet_virtio" "virtio_pci" "virtio_blk" ] ++
+        (pkgs.lib.optional pkgs.stdenv.hostPlatform.isx86 "rtc_cmos");
+      kernel = modulesTree;
+    }
+  ).runInLinuxVM (
+    pkgs.runCommand name
+      {
+        memSize = 1024;
+        QEMU_OPTS = "-drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
+        preVM = ''
+          PATH=$PATH:${pkgs.qemu_kvm}/bin
+          mkdir $out
+
+          rootDiskImage=root.raw
+          qemu-img create -f raw $rootDiskImage ${toString (bootSize + rootSize)}M
+        '';
+
+        postVM = ''
+            ${if formatOpt == "raw" then ''
+            mv $rootDiskImage $out/${rootFilename}
+          '' else ''
+            ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
+          ''}
+            rootDiskImage=$out/${rootFilename}
+            set -x
+            ${postVM}
+        '';
+      } ''
+      export PATH=${tools}:$PATH
+      set -x
+
+      cp -sv /dev/vda /dev/sda
+      cp -sv /dev/vda /dev/xvda
+
+      parted --script /dev/vda -- \
+        mklabel gpt \
+        mkpart no-fs 1MiB 2MiB \
+        set 1 bios_grub on \
+        align-check optimal 1 \
+        mkpart primary fat32 2MiB ${toString bootSize}MiB \
+        align-check optimal 2 \
+        mkpart primary fat32 ${toString bootSize}MiB -1MiB \
+        align-check optimal 3 \
+        print
+
+      sfdisk --dump /dev/vda
+
+
+      zpool create \
+        ${stringifyProperties "  -o" rootPoolProperties} \
+        ${stringifyProperties "  -O" rootPoolFilesystemProperties} \
+        ${rootPoolName} /dev/vda3
+      parted --script /dev/vda -- print
+
+      ${createDatasets}
+      ${mountDatasets}
+
+      mkdir -p /mnt/boot
+      mkfs.vfat -n ESP /dev/vda2
+      mount /dev/vda2 /mnt/boot
+
+      mount
+
+      # Install a configuration.nix
+      mkdir -p /mnt/etc/nixos
+      # `cat` so it is mutable on the fs
+      cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
+
+      export NIX_STATE_DIR=$TMPDIR/state
+      nix-store --load-db < ${closureInfo}/registration
+
+      nixos-install \
+        --root /mnt \
+        --no-root-passwd \
+        --system ${config.system.build.toplevel} \
+        --substituters "" \
+        ${lib.optionalString includeChannel ''--channel ${channelSources}''}
+
+      df -h
+
+      umount /mnt/boot
+      ${unmountDatasets}
+
+      zpool export ${rootPoolName}
+    ''
+  );
+in
+image
diff --git a/nixos/lib/qemu-common.nix b/nixos/lib/qemu-common.nix
index 20bbe9ff5d99f..250f714be0a7a 100644
--- a/nixos/lib/qemu-common.nix
+++ b/nixos/lib/qemu-common.nix
@@ -23,8 +23,8 @@ rec {
 
   qemuBinary = qemuPkg: {
     x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu max";
-    armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -enable-kvm -machine virt -cpu host";
-    aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -enable-kvm -machine virt,gic-version=host -cpu host";
+    armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=kvm:tcg -cpu max";
+    aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=kvm:tcg -cpu max";
     powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
     powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
     x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu max";
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index ab166d7327cea..16ec47df3a464 100644
--- a/nixos/lib/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
@@ -5,6 +5,7 @@ with lib;
 let
   cfg = config.systemd;
   lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
+  systemd = cfg.package;
 in rec {
 
   shellEscape = s: (replaceChars [ "\\" ] [ "\\\\" ] s);
@@ -22,8 +23,9 @@ in rec {
           inherit (unit) text;
         }
         ''
-          mkdir -p $out
-          echo -n "$text" > $out/${shellEscape name}
+          name=${shellEscape name}
+          mkdir -p "$out/$(dirname "$name")"
+          echo -n "$text" > "$out/$name"
         ''
     else
       pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
@@ -31,8 +33,9 @@ in rec {
           allowSubstitutes = false;
         }
         ''
-          mkdir -p $out
-          ln -s /dev/null $out/${shellEscape name}
+          name=${shellEscape name}
+          mkdir -p "$out/$(dirname "$name")"
+          ln -s /dev/null "$out/$name"
         '';
 
   boolValues = [true false "yes" "no"];
@@ -119,10 +122,15 @@ in rec {
         (if isList value then value else [value]))
         as));
 
-  generateUnits = generateUnits' true;
-
-  generateUnits' = allowCollisions: type: units: upstreamUnits: upstreamWants:
-    pkgs.runCommand "${type}-units"
+  generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
+    let
+      typeDir = ({
+        system = "system";
+        initrd = "system";
+        user = "user";
+        nspawn = "nspawn";
+      }).${type};
+    in pkgs.runCommand "${type}-units"
       { preferLocalBuild = true;
         allowSubstitutes = false;
       } ''
@@ -130,7 +138,7 @@ in rec {
 
       # Copy the upstream systemd units we're interested in.
       for i in ${toString upstreamUnits}; do
-        fn=${cfg.package}/example/systemd/${type}/$i
+        fn=${package}/example/systemd/${typeDir}/$i
         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
         if [ -L $fn ]; then
           target="$(readlink "$fn")"
@@ -147,7 +155,7 @@ in rec {
       # Copy .wants links, but only those that point to units that
       # we're interested in.
       for i in ${toString upstreamWants}; do
-        fn=${cfg.package}/example/systemd/${type}/$i
+        fn=${package}/example/systemd/${typeDir}/$i
         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
         x=$out/$(basename $fn)
         mkdir $x
@@ -159,14 +167,14 @@ in rec {
       done
 
       # Symlink all units provided listed in systemd.packages.
-      packages="${toString cfg.packages}"
+      packages="${toString packages}"
 
       # Filter duplicate directories
       declare -A unique_packages
       for k in $packages ; do unique_packages[$k]=1 ; done
 
       for i in ''${!unique_packages[@]}; do
-        for fn in $i/etc/systemd/${type}/* $i/lib/systemd/${type}/*; do
+        for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
           if ! [[ "$fn" =~ .wants$ ]]; then
             if [[ -d "$fn" ]]; then
               targetDir="$out/$(basename "$fn")"
@@ -235,4 +243,184 @@ in rec {
       ''}
     ''; # */
 
+  makeJobScript = name: text:
+    let
+      scriptName = replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
+      out = (pkgs.writeShellScriptBin scriptName ''
+        set -e
+        ${text}
+      '').overrideAttrs (_: {
+        # The derivation name is different from the script file name
+        # to keep the script file name short to avoid cluttering logs.
+        name = "unit-script-${scriptName}";
+      });
+    in "${out}/bin/${scriptName}";
+
+  unitConfig = { config, options, ... }: {
+    config = {
+      unitConfig =
+        optionalAttrs (config.requires != [])
+          { Requires = toString config.requires; }
+        // optionalAttrs (config.wants != [])
+          { Wants = toString config.wants; }
+        // optionalAttrs (config.after != [])
+          { After = toString config.after; }
+        // optionalAttrs (config.before != [])
+          { Before = toString config.before; }
+        // optionalAttrs (config.bindsTo != [])
+          { BindsTo = toString config.bindsTo; }
+        // optionalAttrs (config.partOf != [])
+          { PartOf = toString config.partOf; }
+        // optionalAttrs (config.conflicts != [])
+          { Conflicts = toString config.conflicts; }
+        // optionalAttrs (config.requisite != [])
+          { Requisite = toString config.requisite; }
+        // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
+          { X-Restart-Triggers = toString config.restartTriggers; }
+        // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
+          { X-Reload-Triggers = toString config.reloadTriggers; }
+        // optionalAttrs (config.description != "") {
+          Description = config.description; }
+        // optionalAttrs (config.documentation != []) {
+          Documentation = toString config.documentation; }
+        // optionalAttrs (config.onFailure != []) {
+          OnFailure = toString config.onFailure; }
+        // optionalAttrs (options.startLimitIntervalSec.isDefined) {
+          StartLimitIntervalSec = toString config.startLimitIntervalSec;
+        } // optionalAttrs (options.startLimitBurst.isDefined) {
+          StartLimitBurst = toString config.startLimitBurst;
+        };
+    };
+  };
+
+  serviceConfig = { config, ... }: {
+    config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
+  };
+
+  stage2ServiceConfig = {
+    imports = [ serviceConfig ];
+    # Default path for systemd services. Should be quite minimal.
+    config.path = mkAfter [
+      pkgs.coreutils
+      pkgs.findutils
+      pkgs.gnugrep
+      pkgs.gnused
+      systemd
+    ];
+  };
+
+  stage1ServiceConfig = serviceConfig;
+
+  mountConfig = { config, ... }: {
+    config = {
+      mountConfig =
+        { What = config.what;
+          Where = config.where;
+        } // optionalAttrs (config.type != "") {
+          Type = config.type;
+        } // optionalAttrs (config.options != "") {
+          Options = config.options;
+        };
+    };
+  };
+
+  automountConfig = { config, ... }: {
+    config = {
+      automountConfig =
+        { Where = config.where;
+        };
+    };
+  };
+
+  commonUnitText = def: ''
+      [Unit]
+      ${attrsToSection def.unitConfig}
+    '';
+
+  targetToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text =
+        ''
+          [Unit]
+          ${attrsToSection def.unitConfig}
+        '';
+    };
+
+  serviceToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Service]
+          ${let env = cfg.globalEnvironment // def.environment;
+            in concatMapStrings (n:
+              let s = optionalString (env.${n} != null)
+                "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
+              # systemd max line length is now 1MiB
+              # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
+              in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
+          ${if def ? reloadIfChanged && def.reloadIfChanged then ''
+            X-ReloadIfChanged=true
+          '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
+            X-RestartIfChanged=false
+          '' else ""}
+          ${optionalString (def ? stopIfChanged && !def.stopIfChanged) "X-StopIfChanged=false"}
+          ${attrsToSection def.serviceConfig}
+        '';
+    };
+
+  socketToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Socket]
+          ${attrsToSection def.socketConfig}
+          ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
+          ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
+        '';
+    };
+
+  timerToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Timer]
+          ${attrsToSection def.timerConfig}
+        '';
+    };
+
+  pathToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Path]
+          ${attrsToSection def.pathConfig}
+        '';
+    };
+
+  mountToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Mount]
+          ${attrsToSection def.mountConfig}
+        '';
+    };
+
+  automountToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Automount]
+          ${attrsToSection def.automountConfig}
+        '';
+    };
+
+  sliceToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Slice]
+          ${attrsToSection def.sliceConfig}
+        '';
+    };
 }
diff --git a/nixos/lib/systemd-types.nix b/nixos/lib/systemd-types.nix
new file mode 100644
index 0000000000000..961b6d7f98514
--- /dev/null
+++ b/nixos/lib/systemd-types.nix
@@ -0,0 +1,69 @@
+{ lib, systemdUtils, pkgs }:
+
+with systemdUtils.lib;
+with systemdUtils.unitOptions;
+with lib;
+
+rec {
+  units = with types;
+    attrsOf (submodule ({ name, config, ... }: {
+      options = concreteUnitOptions;
+      config = { unit = mkDefault (systemdUtils.lib.makeUnit name config); };
+    }));
+
+  services = with types; attrsOf (submodule [ stage2ServiceOptions unitConfig stage2ServiceConfig ]);
+  initrdServices = with types; attrsOf (submodule [ stage1ServiceOptions unitConfig stage1ServiceConfig ]);
+
+  targets = with types; attrsOf (submodule [ stage2CommonUnitOptions unitConfig ]);
+  initrdTargets = with types; attrsOf (submodule [ stage1CommonUnitOptions unitConfig ]);
+
+  sockets = with types; attrsOf (submodule [ stage2SocketOptions unitConfig ]);
+  initrdSockets = with types; attrsOf (submodule [ stage1SocketOptions unitConfig ]);
+
+  timers = with types; attrsOf (submodule [ stage2TimerOptions unitConfig ]);
+  initrdTimers = with types; attrsOf (submodule [ stage1TimerOptions unitConfig ]);
+
+  paths = with types; attrsOf (submodule [ stage2PathOptions unitConfig ]);
+  initrdPaths = with types; attrsOf (submodule [ stage1PathOptions unitConfig ]);
+
+  slices = with types; attrsOf (submodule [ stage2SliceOptions unitConfig ]);
+  initrdSlices = with types; attrsOf (submodule [ stage1SliceOptions unitConfig ]);
+
+  mounts = with types; listOf (submodule [ stage2MountOptions unitConfig mountConfig ]);
+  initrdMounts = with types; listOf (submodule [ stage1MountOptions unitConfig mountConfig ]);
+
+  automounts = with types; listOf (submodule [ stage2AutomountOptions unitConfig automountConfig ]);
+  initrdAutomounts = with types; attrsOf (submodule [ stage1AutomountOptions unitConfig automountConfig ]);
+
+  initrdContents = types.attrsOf (types.submodule ({ config, options, name, ... }: {
+    options = {
+      enable = mkEnableOption "copying of this file and symlinking it" // { default = true; };
+
+      target = mkOption {
+        type = types.path;
+        description = ''
+          Path of the symlink.
+        '';
+        default = name;
+      };
+
+      text = mkOption {
+        default = null;
+        type = types.nullOr types.lines;
+        description = "Text of the file.";
+      };
+
+      source = mkOption {
+        type = types.path;
+        description = "Path of the source file.";
+      };
+    };
+
+    config = {
+      source = mkIf (config.text != null) (
+        let name' = "initrd-" + baseNameOf name;
+        in mkDerivedConfig options.text (pkgs.writeText name')
+      );
+    };
+  }));
+}
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 520f2e982a266..cae82433d856b 100644
--- a/nixos/lib/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -20,10 +20,15 @@ in rec {
     merge = loc: defs:
       let
         defs' = filterOverrides defs;
-        defs'' = getValues defs';
       in
-        if isList (head defs'')
-        then concatLists defs''
+        if isList (head defs').value
+        then concatMap (def:
+          if builtins.typeOf def.value == "list"
+          then def.value
+          else
+            throw "The definitions for systemd unit options should be either all lists, representing repeatable options, or all non-lists, but for the option ${showOption loc}, the definitions are a mix of list and non-list ${lib.options.showDefs defs'}"
+        ) defs'
+
         else mergeEqualOption loc defs';
   };
 
@@ -94,443 +99,618 @@ in rec {
 
   };
 
-  commonUnitOptions = sharedOptions // {
-
-    description = mkOption {
-      default = "";
-      type = types.singleLineStr;
-      description = "Description of this unit used in systemd messages and progress indicators.";
-    };
-
-    documentation = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      description = "A list of URIs referencing documentation for this unit or its configuration.";
-    };
-
-    requires = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        Start the specified units when this unit is started, and stop
-        this unit when the specified units are stopped or fail.
-      '';
-    };
-
-    wants = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        Start the specified units when this unit is started.
-      '';
-    };
-
-    after = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        If the specified units are started at the same time as
-        this unit, delay this unit until they have started.
-      '';
-    };
-
-    before = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        If the specified units are started at the same time as
-        this unit, delay them until this unit has started.
-      '';
-    };
-
-    bindsTo = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        Like ‘requires’, but in addition, if the specified units
-        unexpectedly disappear, this unit will be stopped as well.
-      '';
-    };
-
-    partOf = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        If the specified units are stopped or restarted, then this
-        unit is stopped or restarted as well.
-      '';
-    };
-
-    conflicts = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        If the specified units are started, then this unit is stopped
-        and vice versa.
-      '';
-    };
-
-    requisite = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        Similar to requires. However if the units listed are not started,
-        they will not be started and the transaction will fail.
-      '';
-    };
-
-    unitConfig = mkOption {
-      default = {};
-      example = { RequiresMountsFor = "/data"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Unit]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.unit</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
-    };
-
-    restartTriggers = mkOption {
-      default = [];
-      type = types.listOf types.unspecified;
-      description = ''
-        An arbitrary list of items such as derivations.  If any item
-        in the list changes between reconfigurations, the service will
-        be restarted.
-      '';
-    };
-
-    onFailure = mkOption {
-      default = [];
-      type = types.listOf unitNameType;
-      description = ''
-        A list of one or more units that are activated when
-        this unit enters the "failed" state.
-      '';
-    };
+  commonUnitOptions = {
+    options = sharedOptions // {
+
+      description = mkOption {
+        default = "";
+        type = types.singleLineStr;
+        description = "Description of this unit used in systemd messages and progress indicators.";
+      };
+
+      documentation = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = "A list of URIs referencing documentation for this unit or its configuration.";
+      };
+
+      requires = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          Start the specified units when this unit is started, and stop
+          this unit when the specified units are stopped or fail.
+        '';
+      };
+
+      wants = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          Start the specified units when this unit is started.
+        '';
+      };
+
+      after = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          If the specified units are started at the same time as
+          this unit, delay this unit until they have started.
+        '';
+      };
+
+      before = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          If the specified units are started at the same time as
+          this unit, delay them until this unit has started.
+        '';
+      };
+
+      bindsTo = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          Like ‘requires’, but in addition, if the specified units
+          unexpectedly disappear, this unit will be stopped as well.
+        '';
+      };
+
+      partOf = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          If the specified units are stopped or restarted, then this
+          unit is stopped or restarted as well.
+        '';
+      };
+
+      conflicts = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          If the specified units are started, then this unit is stopped
+          and vice versa.
+        '';
+      };
+
+      requisite = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          Similar to requires. However if the units listed are not started,
+          they will not be started and the transaction will fail.
+        '';
+      };
+
+      unitConfig = mkOption {
+        default = {};
+        example = { RequiresMountsFor = "/data"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Unit]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.unit</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+
+      onFailure = mkOption {
+        default = [];
+        type = types.listOf unitNameType;
+        description = ''
+          A list of one or more units that are activated when
+          this unit enters the "failed" state.
+        '';
+      };
+
+      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 startLimitBurst times within an interval time
+           interval are not permitted to start any more.
+         '';
+      };
 
-    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 startLimitBurst times within an interval time
-         interval are not permitted to start any more.
-       '';
-    };
-
   };
 
-
-  serviceOptions = commonUnitOptions // {
-
-    environment = mkOption {
-      default = {};
-      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
-      example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; };
-      description = "Environment variables passed to the service's processes.";
-    };
-
-    path = mkOption {
-      default = [];
-      type = with types; listOf (oneOf [ package str ]);
-      description = ''
-        Packages added to the service's <envar>PATH</envar>
-        environment variable.  Both the <filename>bin</filename>
-        and <filename>sbin</filename> subdirectories of each
-        package are added.
-      '';
+  stage2CommonUnitOptions = {
+    imports = [
+      commonUnitOptions
+    ];
+
+    options = {
+      restartTriggers = mkOption {
+        default = [];
+        type = types.listOf types.unspecified;
+        description = ''
+          An arbitrary list of items such as derivations.  If any item
+          in the list changes between reconfigurations, the service will
+          be restarted.
+        '';
+      };
+
+      reloadTriggers = mkOption {
+        default = [];
+        type = types.listOf unitOption;
+        description = ''
+          An arbitrary list of items such as derivations.  If any item
+          in the list changes between reconfigurations, the service will
+          be reloaded.  If anything but a reload trigger changes in the
+          unit file, the unit will be restarted instead.
+        '';
+      };
     };
+  };
+  stage1CommonUnitOptions = commonUnitOptions;
+
+  serviceOptions = { name, config, ... }: {
+    options = {
+
+      environment = mkOption {
+        default = {};
+        type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+        example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; };
+        description = "Environment variables passed to the service's processes.";
+      };
+
+      path = mkOption {
+        default = [];
+        type = with types; listOf (oneOf [ package str ]);
+        description = ''
+          Packages added to the service's <envar>PATH</envar>
+          environment variable.  Both the <filename>bin</filename>
+          and <filename>sbin</filename> subdirectories of each
+          package are added.
+        '';
+      };
+
+      serviceConfig = mkOption {
+        default = {};
+        example =
+          { RestartSec = 5;
+          };
+        type = types.addCheck (types.attrsOf unitOption) checkService;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Service]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.service</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+
+      script = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Shell commands executed as the service's main process.";
+      };
+
+      scriptArgs = mkOption {
+        type = types.str;
+        default = "";
+        description = "Arguments passed to the main process script.";
+      };
+
+      preStart = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed before the service's main process
+          is started.
+        '';
+      };
+
+      postStart = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed after the service's main process
+          is started.
+        '';
+      };
+
+      reload = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed when the service's main process
+          is reloaded.
+        '';
+      };
+
+      preStop = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed to stop the service.
+        '';
+      };
+
+      postStop = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed after the service's main process
+          has exited.
+        '';
+      };
+
+      jobScripts = mkOption {
+        type = with types; coercedTo path singleton (listOf path);
+        internal = true;
+        description = "A list of all job script derivations of this unit.";
+        default = [];
+      };
+
+    };
+
+    config = mkMerge [
+      (mkIf (config.preStart != "") rec {
+        jobScripts = makeJobScript "${name}-pre-start" config.preStart;
+        serviceConfig.ExecStartPre = [ jobScripts ];
+      })
+      (mkIf (config.script != "") rec {
+        jobScripts = makeJobScript "${name}-start" config.script;
+        serviceConfig.ExecStart = jobScripts + " " + config.scriptArgs;
+      })
+      (mkIf (config.postStart != "") rec {
+        jobScripts = (makeJobScript "${name}-post-start" config.postStart);
+        serviceConfig.ExecStartPost = [ jobScripts ];
+      })
+      (mkIf (config.reload != "") rec {
+        jobScripts = makeJobScript "${name}-reload" config.reload;
+        serviceConfig.ExecReload = jobScripts;
+      })
+      (mkIf (config.preStop != "") rec {
+        jobScripts = makeJobScript "${name}-pre-stop" config.preStop;
+        serviceConfig.ExecStop = jobScripts;
+      })
+      (mkIf (config.postStop != "") rec {
+        jobScripts = makeJobScript "${name}-post-stop" config.postStop;
+        serviceConfig.ExecStopPost = jobScripts;
+      })
+    ];
 
-    serviceConfig = mkOption {
-      default = {};
-      example =
-        { RestartSec = 5;
-        };
-      type = types.addCheck (types.attrsOf unitOption) checkService;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Service]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.service</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
-    };
+  };
 
-    script = mkOption {
-      type = types.lines;
-      default = "";
-      description = "Shell commands executed as the service's main process.";
+  stage2ServiceOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      serviceOptions
+    ];
+
+    options = {
+      restartIfChanged = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the service should be restarted during a NixOS
+          configuration switch if its definition has changed.
+        '';
+      };
+
+      reloadIfChanged = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether the service should be reloaded during a NixOS
+          configuration switch if its definition has changed.  If
+          enabled, the value of <option>restartIfChanged</option> is
+          ignored.
+
+          This option should not be used anymore in favor of
+          <option>reloadTriggers</option> which allows more granular
+          control of when a service is reloaded and when a service
+          is restarted.
+        '';
+      };
+
+      stopIfChanged = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If set, a changed unit is restarted by calling
+          <command>systemctl stop</command> in the old configuration,
+          then <command>systemctl start</command> in the new one.
+          Otherwise, it is restarted in a single step using
+          <command>systemctl restart</command> in the new configuration.
+          The latter is less correct because it runs the
+          <literal>ExecStop</literal> commands from the new
+          configuration.
+        '';
+      };
+
+      startAt = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = "Sun 14:00:00";
+        description = ''
+          Automatically start this unit at the given date/time, which
+          must be in the format described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.  This is equivalent
+          to adding a corresponding timer unit with
+          <option>OnCalendar</option> set to the value given here.
+        '';
+        apply = v: if isList v then v else [ v ];
+      };
     };
+  };
 
-    scriptArgs = mkOption {
-      type = types.str;
-      default = "";
-      description = "Arguments passed to the main process script.";
-    };
+  stage1ServiceOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      serviceOptions
+    ];
+  };
 
-    preStart = mkOption {
-      type = types.lines;
-      default = "";
-      description = ''
-        Shell commands executed before the service's main process
-        is started.
-      '';
-    };
 
-    postStart = mkOption {
-      type = types.lines;
-      default = "";
-      description = ''
-        Shell commands executed after the service's main process
-        is started.
-      '';
+  socketOptions = {
+    options = {
+
+      listenStreams = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = [ "0.0.0.0:993" "/run/my-socket" ];
+        description = ''
+          For each item in this list, a <literal>ListenStream</literal>
+          option in the <literal>[Socket]</literal> section will be created.
+        '';
+      };
+
+      listenDatagrams = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = [ "0.0.0.0:993" "/run/my-socket" ];
+        description = ''
+          For each item in this list, a <literal>ListenDatagram</literal>
+          option in the <literal>[Socket]</literal> section will be created.
+        '';
+      };
+
+      socketConfig = mkOption {
+        default = {};
+        example = { ListenStream = "/run/my-socket"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Socket]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.socket</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
     };
 
-    reload = mkOption {
-      type = types.lines;
-      default = "";
-      description = ''
-        Shell commands executed when the service's main process
-        is reloaded.
-      '';
-    };
+  };
 
-    preStop = mkOption {
-      type = types.lines;
-      default = "";
-      description = ''
-        Shell commands executed to stop the service.
-      '';
-    };
+  stage2SocketOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      socketOptions
+    ];
+  };
 
-    postStop = mkOption {
-      type = types.lines;
-      default = "";
-      description = ''
-        Shell commands executed after the service's main process
-        has exited.
-      '';
-    };
+  stage1SocketOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      socketOptions
+    ];
+  };
 
-    restartIfChanged = mkOption {
-      type = types.bool;
-      default = true;
-      description = ''
-        Whether the service should be restarted during a NixOS
-        configuration switch if its definition has changed.
-      '';
-    };
 
-    reloadIfChanged = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether the service should be reloaded during a NixOS
-        configuration switch if its definition has changed.  If
-        enabled, the value of <option>restartIfChanged</option> is
-        ignored.
-      '';
-    };
+  timerOptions = {
+    options = {
 
-    stopIfChanged = mkOption {
-      type = types.bool;
-      default = true;
-      description = ''
-        If set, a changed unit is restarted by calling
-        <command>systemctl stop</command> in the old configuration,
-        then <command>systemctl start</command> in the new one.
-        Otherwise, it is restarted in a single step using
-        <command>systemctl restart</command> in the new configuration.
-        The latter is less correct because it runs the
-        <literal>ExecStop</literal> commands from the new
-        configuration.
-      '';
-    };
+      timerConfig = mkOption {
+        default = {};
+        example = { OnCalendar = "Sun 14:00:00"; Unit = "foo.service"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Timer]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.timer</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> and
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry> for details.
+        '';
+      };
 
-    startAt = mkOption {
-      type = with types; either str (listOf str);
-      default = [];
-      example = "Sun 14:00:00";
-      description = ''
-        Automatically start this unit at the given date/time, which
-        must be in the format described in
-        <citerefentry><refentrytitle>systemd.time</refentrytitle>
-        <manvolnum>7</manvolnum></citerefentry>.  This is equivalent
-        to adding a corresponding timer unit with
-        <option>OnCalendar</option> set to the value given here.
-      '';
-      apply = v: if isList v then v else [ v ];
     };
+  };
 
+  stage2TimerOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      timerOptions
+    ];
   };
 
+  stage1TimerOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      timerOptions
+    ];
+  };
 
-  socketOptions = commonUnitOptions // {
 
-    listenStreams = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      example = [ "0.0.0.0:993" "/run/my-socket" ];
-      description = ''
-        For each item in this list, a <literal>ListenStream</literal>
-        option in the <literal>[Socket]</literal> section will be created.
-      '';
-    };
+  pathOptions = {
+    options = {
 
-    listenDatagrams = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      example = [ "0.0.0.0:993" "/run/my-socket" ];
-      description = ''
-        For each item in this list, a <literal>ListenDatagram</literal>
-        option in the <literal>[Socket]</literal> section will be created.
-      '';
-    };
+      pathConfig = mkOption {
+        default = {};
+        example = { PathChanged = "/some/path"; Unit = "changedpath.service"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Path]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.path</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
 
-    socketConfig = mkOption {
-      default = {};
-      example = { ListenStream = "/run/my-socket"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Socket]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.socket</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
     };
-
   };
 
+  stage2PathOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      pathOptions
+    ];
+  };
 
-  timerOptions = commonUnitOptions // {
-
-    timerConfig = mkOption {
-      default = {};
-      example = { OnCalendar = "Sun 14:00:00"; Unit = "foo.service"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Timer]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.timer</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> and
-        <citerefentry><refentrytitle>systemd.time</refentrytitle>
-        <manvolnum>7</manvolnum></citerefentry> for details.
-      '';
-    };
-
+  stage1PathOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      pathOptions
+    ];
   };
 
 
-  pathOptions = commonUnitOptions // {
+  mountOptions = {
+    options = {
+
+      what = mkOption {
+        example = "/dev/sda1";
+        type = types.str;
+        description = "Absolute path of device node, file or other resource. (Mandatory)";
+      };
+
+      where = mkOption {
+        example = "/mnt";
+        type = types.str;
+        description = ''
+          Absolute path of a directory of the mount point.
+          Will be created if it doesn't exist. (Mandatory)
+        '';
+      };
+
+      type = mkOption {
+        default = "";
+        example = "ext4";
+        type = types.str;
+        description = "File system type.";
+      };
+
+      options = mkOption {
+        default = "";
+        example = "noatime";
+        type = types.commas;
+        description = "Options used to mount the file system.";
+      };
+
+      mountConfig = mkOption {
+        default = {};
+        example = { DirectoryMode = "0775"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Mount]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.mount</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
 
-    pathConfig = mkOption {
-      default = {};
-      example = { PathChanged = "/some/path"; Unit = "changedpath.service"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Path]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.path</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
     };
+  };
 
+  stage2MountOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      mountOptions
+    ];
   };
 
+  stage1MountOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      mountOptions
+    ];
+  };
 
-  mountOptions = commonUnitOptions // {
+  automountOptions = {
+    options = {
+
+      where = mkOption {
+        example = "/mnt";
+        type = types.str;
+        description = ''
+          Absolute path of a directory of the mount point.
+          Will be created if it doesn't exist. (Mandatory)
+        '';
+      };
+
+      automountConfig = mkOption {
+        default = {};
+        example = { DirectoryMode = "0775"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Automount]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.automount</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
 
-    what = mkOption {
-      example = "/dev/sda1";
-      type = types.str;
-      description = "Absolute path of device node, file or other resource. (Mandatory)";
-    };
-
-    where = mkOption {
-      example = "/mnt";
-      type = types.str;
-      description = ''
-        Absolute path of a directory of the mount point.
-        Will be created if it doesn't exist. (Mandatory)
-      '';
-    };
-
-    type = mkOption {
-      default = "";
-      example = "ext4";
-      type = types.str;
-      description = "File system type.";
     };
+  };
 
-    options = mkOption {
-      default = "";
-      example = "noatime";
-      type = types.commas;
-      description = "Options used to mount the file system.";
-    };
+  stage2AutomountOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      automountOptions
+    ];
+  };
 
-    mountConfig = mkOption {
-      default = {};
-      example = { DirectoryMode = "0775"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Mount]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.mount</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
-    };
+  stage1AutomountOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      automountOptions
+    ];
   };
 
-  automountOptions = commonUnitOptions // {
+  sliceOptions = {
+    options = {
 
-    where = mkOption {
-      example = "/mnt";
-      type = types.str;
-      description = ''
-        Absolute path of a directory of the mount point.
-        Will be created if it doesn't exist. (Mandatory)
-      '';
-    };
+      sliceConfig = mkOption {
+        default = {};
+        example = { MemoryMax = "2G"; };
+        type = types.attrsOf unitOption;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Slice]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.slice</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
 
-    automountConfig = mkOption {
-      default = {};
-      example = { DirectoryMode = "0775"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Automount]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.automount</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
     };
   };
 
-  targetOptions = commonUnitOptions;
-
-  sliceOptions = commonUnitOptions // {
-
-    sliceConfig = mkOption {
-      default = {};
-      example = { MemoryMax = "2G"; };
-      type = types.attrsOf unitOption;
-      description = ''
-        Each attribute in this set specifies an option in the
-        <literal>[Slice]</literal> section of the unit.  See
-        <citerefentry><refentrytitle>systemd.slice</refentrytitle>
-        <manvolnum>5</manvolnum></citerefentry> for details.
-      '';
-    };
+  stage2SliceOptions = {
+    imports = [
+      stage2CommonUnitOptions
+      sliceOptions
+    ];
+  };
 
+  stage1SliceOptions = {
+    imports = [
+      stage1CommonUnitOptions
+      sliceOptions
+    ];
   };
 
 }
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
index 3aee913431890..e3786622c3c58 100644
--- a/nixos/lib/test-driver/default.nix
+++ b/nixos/lib/test-driver/default.nix
@@ -10,6 +10,7 @@
 , socat
 , tesseract4
 , vde2
+, extraPythonPackages ? (_ : [])
 }:
 
 python3Packages.buildPythonApplication rec {
@@ -17,14 +18,25 @@ python3Packages.buildPythonApplication rec {
   version = "1.1";
   src = ./.;
 
-  propagatedBuildInputs = [ coreutils netpbm python3Packages.colorama python3Packages.ptpython qemu_pkg socat vde2 ]
-    ++ (lib.optionals enableOCR [ imagemagick_light tesseract4 ]);
+  propagatedBuildInputs = [
+    coreutils
+    netpbm
+    python3Packages.colorama
+    python3Packages.ptpython
+    qemu_pkg
+    socat
+    vde2
+  ]
+    ++ (lib.optionals enableOCR [ imagemagick_light tesseract4 ])
+    ++ extraPythonPackages python3Packages;
 
   doCheck = true;
   checkInputs = with python3Packages; [ mypy pylint black ];
   checkPhase = ''
     mypy --disallow-untyped-defs \
           --no-implicit-optional \
+          --pretty \
+          --no-color-output \
           --ignore-missing-imports ${src}/test_driver
     pylint --errors-only --enable=unused-import ${src}/test_driver
     black --check --diff ${src}/test_driver
diff --git a/nixos/lib/test-driver/setup.py b/nixos/lib/test-driver/setup.py
index 476c7b2dab2a4..1719b988db689 100644
--- a/nixos/lib/test-driver/setup.py
+++ b/nixos/lib/test-driver/setup.py
@@ -4,6 +4,7 @@ setup(
   name="nixos-test-driver",
   version='1.1',
   packages=find_packages(),
+  package_data={"test_driver": ["py.typed"]},
   entry_points={
     "console_scripts": [
       "nixos-test-driver=test_driver:main",
diff --git a/nixos/lib/test-driver/test_driver/driver.py b/nixos/lib/test-driver/test_driver/driver.py
index 880b1c5fdec0d..e32f6810ca87f 100644
--- a/nixos/lib/test-driver/test_driver/driver.py
+++ b/nixos/lib/test-driver/test_driver/driver.py
@@ -55,6 +55,7 @@ class Driver:
         tmp_dir = get_tmp_dir()
 
         with rootlog.nested("start all VLans"):
+            vlans = list(set(vlans))
             self.vlans = [VLan(nr, tmp_dir) for nr in vlans]
 
         def cmd(scripts: List[str]) -> Iterator[NixStartScript]:
@@ -85,7 +86,7 @@ class Driver:
 
     def subtest(self, name: str) -> Iterator[None]:
         """Group logs under a given test name"""
-        with rootlog.nested(name):
+        with rootlog.nested("subtest: " + name):
             try:
                 yield
                 return True
diff --git a/nixos/lib/test-driver/test_driver/logger.py b/nixos/lib/test-driver/test_driver/logger.py
index 5b3091a5129c3..59ed295472315 100644
--- a/nixos/lib/test-driver/test_driver/logger.py
+++ b/nixos/lib/test-driver/test_driver/logger.py
@@ -1,4 +1,4 @@
-from colorama import Style
+from colorama import Style, Fore
 from contextlib import contextmanager
 from typing import Any, Dict, Iterator
 from queue import Queue, Empty
@@ -81,7 +81,11 @@ class Logger:
 
     @contextmanager
     def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]:
-        self._eprint(self.maybe_prefix(message, attributes))
+        self._eprint(
+            self.maybe_prefix(
+                Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes
+            )
+        )
 
         self.xml.startElement("nest", attrs={})
         self.xml.startElement("head", attributes)
diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py
index b730c4b44d7fc..3ff3cf5645f82 100644
--- a/nixos/lib/test-driver/test_driver/machine.py
+++ b/nixos/lib/test-driver/test_driver/machine.py
@@ -198,7 +198,7 @@ class StartCommand:
     ) -> subprocess.Popen:
         return subprocess.Popen(
             self.cmd(monitor_socket_path, shell_socket_path),
-            stdin=subprocess.DEVNULL,
+            stdin=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             shell=True,
@@ -241,9 +241,15 @@ class LegacyStartCommand(StartCommand):
         cdrom: Optional[str] = None,
         usb: Optional[str] = None,
         bios: Optional[str] = None,
+        qemuBinary: Optional[str] = None,
         qemuFlags: Optional[str] = None,
     ):
-        self._cmd = "qemu-kvm -m 384"
+        if qemuBinary is not None:
+            self._cmd = qemuBinary
+        else:
+            self._cmd = "qemu-kvm"
+
+        self._cmd += " -m 384"
 
         # networking
         net_backend = "-netdev user,id=net0"
@@ -381,6 +387,7 @@ class Machine:
             cdrom=args.get("cdrom"),
             usb=args.get("usb"),
             bios=args.get("bios"),
+            qemuBinary=args.get("qemuBinary"),
             qemuFlags=args.get("qemuFlags"),
         )
 
@@ -519,10 +526,17 @@ class Machine:
         self.run_callbacks()
         self.connect()
 
+        # Always run command with shell opts
+        command = f"set -euo pipefail; {command}"
+
+        timeout_str = ""
         if timeout is not None:
-            command = "timeout {} sh -c {}".format(timeout, shlex.quote(command))
+            timeout_str = f"timeout {timeout}"
+
+        out_command = (
+            f"{timeout_str} sh -c {shlex.quote(command)} | (base64 --wrap 0; echo)\n"
+        )
 
-        out_command = f"( set -euo pipefail; {command} ) | (base64 --wrap 0; echo)\n"
         assert self.shell
         self.shell.send(out_command.encode())
 
@@ -551,6 +565,28 @@ class Machine:
             pass_fds=[self.shell.fileno()],
         )
 
+    def console_interact(self) -> None:
+        """Allows you to interact with QEMU's stdin
+
+        The shell can be exited with Ctrl+D. Note that Ctrl+C is not allowed to be used.
+        QEMU's stdout is read line-wise.
+
+        Should only be used during test development, not in the production test."""
+        self.log("Terminal is ready (there is no prompt):")
+
+        assert self.process
+        assert self.process.stdin
+
+        while True:
+            try:
+                char = sys.stdin.buffer.read(1)
+            except KeyboardInterrupt:
+                break
+            if char == b"":  # ctrl+d
+                self.log("Closing connection to the console")
+                break
+            self.send_console(char.decode())
+
     def succeed(self, *commands: str, timeout: Optional[int] = None) -> str:
         """Execute each command and check that it succeeds."""
         output = ""
@@ -646,7 +682,7 @@ class Machine:
         with self.nested("waiting for {} to appear on tty {}".format(regexp, tty)):
             retry(tty_matches)
 
-    def send_chars(self, chars: List[str]) -> None:
+    def send_chars(self, chars: str) -> None:
         with self.nested("sending keys ‘{}‘".format(chars)):
             for char in chars:
                 self.send_key(char)
@@ -827,6 +863,12 @@ class Machine:
         self.send_monitor_command("sendkey {}".format(key))
         time.sleep(0.01)
 
+    def send_console(self, chars: str) -> None:
+        assert self.process
+        assert self.process.stdin
+        self.process.stdin.write(chars.encode())
+        self.process.stdin.flush()
+
     def start(self) -> None:
         if self.booted:
             return
diff --git a/nixos/lib/test-driver/test_driver/py.typed b/nixos/lib/test-driver/test_driver/py.typed
new file mode 100644
index 0000000000000..e69de29bb2d1d
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/py.typed
diff --git a/nixos/lib/test-driver/test_driver/vlan.py b/nixos/lib/test-driver/test_driver/vlan.py
index e5c8f07b4edf4..f2a7f250d1d2f 100644
--- a/nixos/lib/test-driver/test_driver/vlan.py
+++ b/nixos/lib/test-driver/test_driver/vlan.py
@@ -32,8 +32,12 @@ class VLan:
         rootlog.info("start vlan")
         pty_master, pty_slave = pty.openpty()
 
+        # The --hub is required for the scenario determined by
+        # nixos/tests/networking.nix vlan-ping.
+        # VLAN Tagged traffic (802.1Q) seams to be blocked if a vde_switch is
+        # used without the hub mode (flood packets to all ports).
         self.process = subprocess.Popen(
-            ["vde_switch", "-s", self.socket_dir, "--dirmode", "0700"],
+            ["vde_switch", "-s", self.socket_dir, "--dirmode", "0700", "--hub"],
             stdin=pty_slave,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
@@ -50,7 +54,7 @@ class VLan:
         if not (self.socket_dir / "ctl").exists():
             rootlog.error("cannot start vde_switch")
 
-        rootlog.info(f"running vlan (pid {self.pid})")
+        rootlog.info(f"running vlan (pid {self.pid}; ctl {self.socket_dir})")
 
     def __del__(self) -> None:
         rootlog.info(f"kill vlan (pid {self.pid})")
diff --git a/nixos/lib/test-script-prepend.py b/nixos/lib/test-script-prepend.py
new file mode 100644
index 0000000000000..15e59ce01047d
--- /dev/null
+++ b/nixos/lib/test-script-prepend.py
@@ -0,0 +1,42 @@
+# This file contains type hints that can be prepended to Nix test scripts so they can be type
+# checked.
+
+from test_driver.driver import Driver
+from test_driver.vlan import VLan
+from test_driver.machine import Machine
+from test_driver.logger import Logger
+from typing import Callable, Iterator, ContextManager, Optional, List, Dict, Any, Union
+from typing_extensions import Protocol
+from pathlib import Path
+
+
+class RetryProtocol(Protocol):
+    def __call__(self, fn: Callable, timeout: int = 900) -> None:
+        raise Exception("This is just type information for the Nix test driver")
+
+
+class PollingConditionProtocol(Protocol):
+    def __call__(
+        self,
+        fun_: Optional[Callable] = None,
+        *,
+        seconds_interval: float = 2.0,
+        description: Optional[str] = None,
+    ) -> Union[Callable[[Callable], ContextManager], ContextManager]:
+        raise Exception("This is just type information for the Nix test driver")
+
+
+start_all: Callable[[], None]
+subtest: Callable[[str], ContextManager[None]]
+retry: RetryProtocol
+test_script: Callable[[], None]
+machines: List[Machine]
+vlans: List[VLan]
+driver: Driver
+log: Logger
+create_machine: Callable[[Dict[str, Any]], Machine]
+run_tests: Callable[[], None]
+join_all: Callable[[], None]
+serial_stdout_off: Callable[[], None]
+serial_stdout_on: Callable[[], None]
+polling_condition: PollingConditionProtocol
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 0d3c3a89e7836..4bb1689ffd789 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -50,14 +50,16 @@ rec {
     , qemu_pkg ? pkgs.qemu_test
     , enableOCR ? false
     , skipLint ? false
+    , skipTypeCheck ? false
     , passthru ? {}
     , interactive ? false
+    , extraPythonPackages ? (_ :[])
   }:
     let
       # Reifies and correctly wraps the python test driver for
       # the respective qemu version and with or without ocr support
       testDriver = pkgs.callPackage ./test-driver {
-        inherit enableOCR;
+        inherit enableOCR extraPythonPackages;
         qemu_pkg = qemu_test;
         imagemagick_light = imagemagick_light.override { inherit libtiff; };
         tesseract4 = tesseract4.override { enableLanguages = [ "eng" ]; };
@@ -85,7 +87,7 @@ rec {
 
       nodeHostNames = let
         nodesList = map (c: c.config.system.name) (lib.attrValues nodes);
-      in nodesList ++ lib.optional (lib.length nodesList == 1) "machine";
+      in nodesList ++ lib.optional (lib.length nodesList == 1 && !lib.elem "machine" nodesList) "machine";
 
       # TODO: This is an implementation error and needs fixing
       # the testing famework cannot legitimately restrict hostnames further
@@ -100,6 +102,9 @@ rec {
         then testScript { inherit nodes; }
         else testScript;
 
+      uniqueVlans = lib.unique (builtins.concatLists vlans);
+      vlanNames = map (i: "vlan${toString i}: VLan;") uniqueVlans;
+      machineNames = map (name: "${name}: Machine;") nodeHostNames;
     in
     if lib.length invalidNodeNames > 0 then
       throw ''
@@ -113,18 +118,35 @@ rec {
     else lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
       {
         inherit testName;
-        nativeBuildInputs = [ makeWrapper ];
+        nativeBuildInputs = [ makeWrapper mypy ];
+        buildInputs = [ testDriver ];
         testScript = testScript';
         preferLocalBuild = true;
         passthru = passthru // {
           inherit nodes;
         };
+        meta.mainProgram = "nixos-test-driver";
       }
       ''
         mkdir -p $out/bin
 
         vmStartScripts=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
-        echo -n "$testScript" > $out/test-script
+
+        ${lib.optionalString (!skipTypeCheck) ''
+          # prepend type hints so the test script can be type checked with mypy
+          cat "${./test-script-prepend.py}" >> testScriptWithTypes
+          echo "${builtins.toString machineNames}" >> testScriptWithTypes
+          echo "${builtins.toString vlanNames}" >> testScriptWithTypes
+          echo -n "$testScript" >> testScriptWithTypes
+
+          mypy  --no-implicit-optional \
+                --pretty \
+                --no-color-output \
+                testScriptWithTypes
+        ''}
+
+        echo -n "$testScript" >> $out/test-script
+
         ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
 
         ${testDriver}/bin/generate-driver-symbols
@@ -146,26 +168,30 @@ rec {
 
   # Make a full-blown test
   makeTest =
-    { testScript
+    { machine ? null
+    , nodes ? {}
+    , testScript
     , enableOCR ? false
     , name ? "unnamed"
+    , skipTypeCheck ? false
       # Skip linting (mainly intended for faster dev cycles)
     , skipLint ? false
     , passthru ? {}
+    , meta ? {}
     , # For meta.position
       pos ? # position used in error messages and for meta.position
-        (if t.meta.description or null != null
-          then builtins.unsafeGetAttrPos "description" t.meta
+        (if meta.description or null != null
+          then builtins.unsafeGetAttrPos "description" meta
           else builtins.unsafeGetAttrPos "testScript" t)
-    , ...
+    , extraPythonPackages ? (_ : [])
     } @ t:
     let
-      nodes = qemu_pkg:
+      mkNodes = qemu_pkg:
         let
           testScript' =
             # Call the test script with the computed nodes.
             if lib.isFunction testScript
-            then testScript { nodes = nodes qemu_pkg; }
+            then testScript { nodes = mkNodes qemu_pkg; }
             else testScript;
 
           build-vms = import ./build-vms.nix {
@@ -204,34 +230,31 @@ rec {
             )];
           };
         in
+          lib.warnIf (t?machine) "In test `${name}': The `machine' attribute in NixOS tests (pkgs.nixosTest / make-test-python.nix / testing-python.nix / makeTest) is deprecated. Please use the equivalent `nodes.machine'."
           build-vms.buildVirtualNetwork (
-              t.nodes or (if t ? machine then { machine = t.machine; } else { })
+              nodes // lib.optionalAttrs (machine != null) { inherit machine; }
           );
 
       driver = setupDriverForTest {
-        inherit testScript enableOCR skipLint passthru;
+        inherit testScript enableOCR skipTypeCheck skipLint passthru extraPythonPackages;
         testName = name;
         qemu_pkg = pkgs.qemu_test;
-        nodes = nodes pkgs.qemu_test;
+        nodes = mkNodes pkgs.qemu_test;
       };
       driverInteractive = setupDriverForTest {
-        inherit testScript enableOCR skipLint passthru;
+        inherit testScript enableOCR skipTypeCheck skipLint passthru extraPythonPackages;
         testName = name;
         qemu_pkg = pkgs.qemu;
-        nodes = nodes pkgs.qemu;
+        nodes = mkNodes pkgs.qemu;
         interactive = true;
       };
 
-      test =
-        let
-          passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
-            meta = (drv.meta or { }) // t.meta;
-          };
-        in passMeta (runTests { inherit driver pos driverInteractive; });
+      test = lib.addMetaAttrs meta (runTests { inherit driver pos driverInteractive; });
 
     in
       test // {
-        inherit test driver driverInteractive nodes;
+        inherit test driver driverInteractive;
+        inherit (driver) nodes;
       };
 
   abortForFunction = functionName: abort ''The ${functionName} function was
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
index 190c4db4d49d4..d7671a374999a 100644
--- a/nixos/lib/utils.nix
+++ b/nixos/lib/utils.nix
@@ -45,6 +45,26 @@ rec {
    replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"]
    (removePrefix "/" s);
 
+  # Quotes an argument for use in Exec* service lines.
+  # systemd accepts "-quoted strings with escape sequences, toJSON produces
+  # a subset of these.
+  # Additionally we escape % to disallow expansion of % specifiers. Any lone ;
+  # in the input will be turned it ";" and thus lose its special meaning.
+  # Every $ is escaped to $$, this makes it unnecessary to disable environment
+  # substitution for the directive.
+  escapeSystemdExecArg = arg:
+    let
+      s = if builtins.isPath arg then "${arg}"
+        else if builtins.isString arg then arg
+        else if builtins.isInt arg || builtins.isFloat arg then toString arg
+        else throw "escapeSystemdExecArg only allows strings, paths and numbers";
+    in
+      replaceChars [ "%" "$" ] [ "%%" "$$" ] (builtins.toJSON s);
+
+  # Quotes a list of arguments into a single string for use in a Exec*
+  # line.
+  escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
+
   # Returns a system path for a given shell package
   toShellPath = shell:
     if types.shellPackage.check shell then
@@ -150,7 +170,8 @@ rec {
         rm '${output}'
       fi
 
-      inherit_errexit_restore=$(shopt -p inherit_errexit)
+      inherit_errexit_enabled=0
+      shopt -pq inherit_errexit && inherit_errexit_enabled=1
       shopt -s inherit_errexit
     ''
     + concatStringsSep
@@ -170,11 +191,28 @@ rec {
       ' <<'EOF'
       ${builtins.toJSON set}
       EOF
-      $inherit_errexit_restore
+      (( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
     '';
 
+  /* Remove packages of packagesToRemove from packages, based on their names.
+     Relies on package names and has quadratic complexity so use with caution!
+
+     Type:
+       removePackagesByName :: [package] -> [package] -> [package]
+
+     Example:
+       removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
+       => [ nautilus ]
+  */
+  removePackagesByName = packages: packagesToRemove:
+    let
+      namesToRemove = map lib.getName packagesToRemove;
+    in
+      lib.filter (x: !(builtins.elem (lib.getName x) namesToRemove)) packages;
+
   systemdUtils = {
     lib = import ./systemd-lib.nix { inherit lib config pkgs; };
     unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
+    types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
   };
 }
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index 6358ec68f7cf6..2d89db0a7f34a 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -73,7 +73,7 @@ in {
         }
       '';
 
-    zfsBuilder = import ../../../lib/make-zfs-image.nix {
+    zfsBuilder = import ../../../lib/make-multi-disk-zfs-image.nix {
       inherit lib config configFile;
       inherit (cfg) contents format name;
       pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
diff --git a/nixos/maintainers/scripts/ec2/create-amis.sh b/nixos/maintainers/scripts/ec2/create-amis.sh
index 797fe03e20952..0c1656efaf1ca 100755
--- a/nixos/maintainers/scripts/ec2/create-amis.sh
+++ b/nixos/maintainers/scripts/ec2/create-amis.sh
@@ -26,12 +26,32 @@ var ${home_region:=eu-west-1}
 var ${bucket:=nixos-amis}
 var ${service_role_name:=vmimport}
 
-var ${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
+# Output of the command:
+# > aws ec2 describe-regions --all-regions --query "Regions[].{Name:RegionName}" --output text | sort
+var ${regions:=
+         af-south-1
+         ap-east-1
+         ap-northeast-1
+         ap-northeast-2
+         ap-northeast-3
+         ap-south-1
+         ap-southeast-1
+         ap-southeast-2
+         ap-southeast-3
          ca-central-1
-         ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2
-         ap-south-1 ap-east-1
-         sa-east-1}
+         eu-central-1
+         eu-north-1
+         eu-south-1
+         eu-west-1
+         eu-west-2
+         eu-west-3
+         me-south-1
+         sa-east-1
+         us-east-1
+         us-east-2
+         us-west-1
+         us-west-2
+     }
 
 regions=($regions)
 
diff --git a/nixos/maintainers/scripts/lxd/lxd-image.nix b/nixos/maintainers/scripts/lxd/lxd-image.nix
index c76b9fcc7f779..6aa3f2f558471 100644
--- a/nixos/maintainers/scripts/lxd/lxd-image.nix
+++ b/nixos/maintainers/scripts/lxd/lxd-image.nix
@@ -27,7 +27,7 @@ with lib;
   networking.useDHCP = false;
   networking.interfaces.eth0.useDHCP = true;
 
-  # As this is intended as a stadalone image, undo some of the minimal profile stuff
+  # As this is intended as a standalone image, undo some of the minimal profile stuff
   documentation.enable = true;
   documentation.nixos.enable = true;
   environment.noXlibs = false;
diff --git a/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix b/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix
new file mode 100644
index 0000000000000..d62a560642d03
--- /dev/null
+++ b/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix
@@ -0,0 +1,101 @@
+# nix-build '<nixpkgs/nixos>' -A config.system.build.openstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/openstack-image.nix ]; }"
+
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkOption types;
+  copyChannel = true;
+  cfg = config.openstackImage;
+  imageBootMode = if config.openstack.efi then "uefi" else "legacy-bios";
+in
+{
+  imports = [
+    ../../../modules/virtualisation/openstack-config.nix
+  ] ++ (lib.optional copyChannel ../../../modules/installer/cd-dvd/channel.nix);
+
+
+  options.openstackImage = {
+    name = mkOption {
+      type = types.str;
+      description = "The name of the generated derivation";
+      default = "nixos-openstack-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+    };
+
+    sizeMB = mkOption {
+      type = types.int;
+      default = 8192;
+      description = "The size in MB of the image";
+    };
+
+    format = mkOption {
+      type = types.enum [ "raw" "qcow2" ];
+      default = "qcow2";
+      description = "The image format to output";
+    };
+  };
+
+  config = {
+    documentation.enable = copyChannel;
+    openstack = {
+      efi = true;
+      zfs = {
+        enable = true;
+        datasets = {
+          "tank/system/root".mount = "/";
+          "tank/system/var".mount = "/var";
+          "tank/local/nix".mount = "/nix";
+          "tank/user/home".mount = "/home";
+        };
+      };
+    };
+
+    system.build.openstackImage = import ../../../lib/make-single-disk-zfs-image.nix {
+      inherit lib config;
+      inherit (cfg) contents format name;
+      pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
+
+      configFile = pkgs.writeText "configuration.nix"
+        ''
+          { modulesPath, ... }: {
+            imports = [ "''${modulesPath}/virtualisation/openstack-config.nix" ];
+            openstack.zfs.enable = true;
+          }
+        '';
+
+      includeChannel = copyChannel;
+
+      bootSize = 1000;
+
+      rootSize = cfg.sizeMB;
+      rootPoolProperties = {
+        ashift = 12;
+        autoexpand = "on";
+      };
+
+      datasets = config.openstack.zfs.datasets;
+
+      postVM = ''
+         extension=''${rootDiskImage##*.}
+         friendlyName=$out/${cfg.name}
+         rootDisk="$friendlyName.root.$extension"
+         mv "$rootDiskImage" "$rootDisk"
+
+         mkdir -p $out/nix-support
+         echo "file ${cfg.format} $rootDisk" >> $out/nix-support/hydra-build-products
+
+        ${pkgs.jq}/bin/jq -n \
+          --arg system_label ${lib.escapeShellArg config.system.nixos.label} \
+          --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \
+          --arg root_logical_bytes "$(${pkgs.qemu}/bin/qemu-img info --output json "$rootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \
+          --arg boot_mode "${imageBootMode}" \
+          --arg root "$rootDisk" \
+         '{}
+           | .label = $system_label
+           | .boot_mode = $boot_mode
+           | .system = $system
+           | .disks.root.logical_bytes = $root_logical_bytes
+           | .disks.root.file = $root
+           ' > $out/nix-support/image-info.json
+      '';
+    };
+  };
+}
diff --git a/nixos/maintainers/scripts/openstack/openstack-image.nix b/nixos/maintainers/scripts/openstack/openstack-image.nix
index 3255e7f3d44d6..6728a98758b83 100644
--- a/nixos/maintainers/scripts/openstack/openstack-image.nix
+++ b/nixos/maintainers/scripts/openstack/openstack-image.nix
@@ -1,17 +1,18 @@
 # nix-build '<nixpkgs/nixos>' -A config.system.build.openstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/openstack-image.nix ]; }"
 
 { config, lib, pkgs, ... }:
-
-with lib;
-
+let
+  copyChannel = true;
+in
 {
-  imports =
-    [ ../../../modules/installer/cd-dvd/channel.nix
-      ../../../modules/virtualisation/openstack-config.nix
-    ];
+  imports = [
+    ../../../modules/virtualisation/openstack-config.nix
+  ] ++ (lib.optional copyChannel ../../../modules/installer/cd-dvd/channel.nix);
+
+  documentation.enable = copyChannel;
 
   system.build.openstackImage = import ../../../lib/make-disk-image.nix {
-    inherit lib config;
+    inherit lib config copyChannel;
     additionalSpace = "1024M";
     pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
     format = "qcow2";
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
index 168bebd8d06a3..97e6405db91e1 100644
--- a/nixos/modules/config/console.nix
+++ b/nixos/modules/config/console.nix
@@ -12,7 +12,7 @@ let
 
   optimizedKeymap = pkgs.runCommand "keymap" {
     nativeBuildInputs = [ pkgs.buildPackages.kbd ];
-    LOADKEYS_KEYMAP_PATH = "${consoleEnv}/share/keymaps/**";
+    LOADKEYS_KEYMAP_PATH = "${consoleEnv pkgs.kbd}/share/keymaps/**";
     preferLocalBuild = true;
   } ''
     loadkeys -b ${optionalString isUnicode "-u"} "${cfg.keyMap}" > $out
@@ -24,9 +24,9 @@ let
     FONT=${cfg.font}
   '';
 
-  consoleEnv = pkgs.buildEnv {
+  consoleEnv = kbd: pkgs.buildEnv {
     name = "console-env";
-    paths = [ pkgs.kbd ] ++ cfg.packages;
+    paths = [ kbd ] ++ cfg.packages;
     pathsToLink = [
       "/share/consolefonts"
       "/share/consoletrans"
@@ -46,9 +46,9 @@ in
       type = with types; either str path;
       default = "Lat2-Terminus16";
       example = "LatArCyrHeb-16";
-      description = ''
+      description = mdDoc ''
         The font used for the virtual consoles.  Leave empty to use
-        whatever the <command>setfont</command> program considers the
+        whatever the {command}`setfont` program considers the
         default font.
         Can be either a font name or a path to a PSF font file.
       '';
@@ -136,9 +136,9 @@ in
         # virtual consoles.
         environment.etc."vconsole.conf".source = vconsoleConf;
         # Provide kbd with additional packages.
-        environment.etc.kbd.source = "${consoleEnv}/share";
+        environment.etc.kbd.source = "${consoleEnv pkgs.kbd}/share";
 
-        boot.initrd.preLVMCommands = mkBefore ''
+        boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (mkBefore ''
           kbd_mode ${if isUnicode then "-u" else "-a"} -C /dev/console
           printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
           loadkmap < ${optimizedKeymap}
@@ -146,12 +146,26 @@ in
           ${optionalString cfg.earlySetup ''
             setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
           ''}
-        '';
+        '');
+
+        boot.initrd.systemd.contents = {
+          "/etc/vconsole.conf".source = vconsoleConf;
+          # Add everything if we want full console setup...
+          "/etc/kbd" = lib.mkIf cfg.earlySetup { source = "${consoleEnv config.boot.initrd.systemd.package.kbd}/share"; };
+          # ...but only the keymaps if we don't
+          "/etc/kbd/keymaps" = lib.mkIf (!cfg.earlySetup) { source = "${consoleEnv config.boot.initrd.systemd.package.kbd}/share/keymaps"; };
+        };
+        boot.initrd.systemd.storePaths = [
+          "${config.boot.initrd.systemd.package}/lib/systemd/systemd-vconsole-setup"
+          "${config.boot.initrd.systemd.package.kbd}/bin/setfont"
+          "${config.boot.initrd.systemd.package.kbd}/bin/loadkeys"
+          "${config.boot.initrd.systemd.package.kbd.gzip}/bin/gzip" # keyboard layouts are compressed
+        ];
 
         systemd.services.reload-systemd-vconsole-setup =
           { description = "Reset console on configuration changes";
             wantedBy = [ "multi-user.target" ];
-            restartTriggers = [ vconsoleConf consoleEnv ];
+            restartTriggers = [ vconsoleConf (consoleEnv pkgs.kbd) ];
             reloadIfChanged = true;
             serviceConfig =
               { RemainAfterExit = true;
@@ -169,13 +183,13 @@ in
         ];
       })
 
-      (mkIf cfg.earlySetup {
+      (mkIf (cfg.earlySetup && !config.boot.initrd.systemd.enable) {
         boot.initrd.extraUtilsCommands = ''
           mkdir -p $out/share/consolefonts
           ${if substring 0 1 cfg.font == "/" then ''
             font="${cfg.font}"
           '' else ''
-            font="$(echo ${consoleEnv}/share/consolefonts/${cfg.font}.*)"
+            font="$(echo ${consoleEnv pkgs.kbd}/share/consolefonts/${cfg.font}.*)"
           ''}
           if [[ $font == *.gz ]]; then
             gzip -cd $font > $out/share/consolefonts/font.psf
diff --git a/nixos/modules/config/debug-info.nix b/nixos/modules/config/debug-info.nix
index 2942ae5905d10..78de26fda4403 100644
--- a/nixos/modules/config/debug-info.nix
+++ b/nixos/modules/config/debug-info.nix
@@ -9,21 +9,20 @@ with lib;
     environment.enableDebugInfo = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = mdDoc ''
         Some NixOS packages provide debug symbols. However, these are
         not included in the system closure by default to save disk
         space. Enabling this option causes the debug symbols to appear
-        in <filename>/run/current-system/sw/lib/debug/.build-id</filename>,
-        where tools such as <command>gdb</command> can find them.
+        in {file}`/run/current-system/sw/lib/debug/.build-id`,
+        where tools such as {command}`gdb` can find them.
         If you need debug symbols for a package that doesn't
         provide them by default, you can enable them as follows:
-        <programlisting>
-        nixpkgs.config.packageOverrides = pkgs: {
-          hello = pkgs.hello.overrideAttrs (oldAttrs: {
-            separateDebugInfo = true;
-          });
-        };
-        </programlisting>
+
+            nixpkgs.config.packageOverrides = pkgs: {
+              hello = pkgs.hello.overrideAttrs (oldAttrs: {
+                separateDebugInfo = true;
+              });
+            };
       '';
     };
 
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index 1e68fef7ce74c..a10a8c6428a10 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -65,7 +65,7 @@ let
           ${fcBool cfg.hinting.autohint}
         </edit>
         <edit mode="append" name="hintstyle">
-          <const>hintslight</const>
+          <const>${cfg.hinting.style}</const>
         </edit>
         <edit mode="append" name="antialias">
           ${fcBool cfg.antialias}
@@ -226,7 +226,6 @@ in
     (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "useEmbeddedBitmaps" ] [ "fonts" "fontconfig" "useEmbeddedBitmaps" ])
     (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "forceAutohint" ] [ "fonts" "fontconfig" "forceAutohint" ])
     (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "renderMonoTTFAsBitmap" ] [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ])
-    (mkRemovedOptionModule [ "fonts" "fontconfig" "hinting" "style" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "forceAutohint" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "dpi" ] "Use display server-specific options")
@@ -349,6 +348,20 @@ in
               fonts, but better than unhinted fonts.
             '';
           };
+
+          style = mkOption {
+            type = types.enum [ "hintnone" "hintslight" "hintmedium" "hintfull" ];
+            default = "hintslight";
+            description = ''
+              Hintstyle is the amount of font reshaping done to line up
+              to the grid.
+
+              hintslight will make the font more fuzzy to line up to the grid
+              but will be better in retaining font shape, while hintfull will
+              be a crisp font that aligns well to the pixel grid but will lose
+              a greater amount of font shape.
+            '';
+          };
         };
 
         includeUserConf = mkOption {
diff --git a/nixos/modules/config/fonts/fonts.nix b/nixos/modules/config/fonts/fonts.nix
index 04952898cb761..adc6654afc79b 100644
--- a/nixos/modules/config/fonts/fonts.nix
+++ b/nixos/modules/config/fonts/fonts.nix
@@ -39,11 +39,6 @@ let
   defaultXFonts =
     [ (if hasHidpi then fontcursormisc_hidpi else pkgs.xorg.fontcursormisc)
       pkgs.xorg.fontmiscmisc
-    ] ++ optionals (config.nixpkgs.config.allowUnfree or false)
-    [ # these are unfree, and will make usage with xserver fail
-      pkgs.xorg.fontbhlucidatypewriter100dpi
-      pkgs.xorg.fontbhlucidatypewriter75dpi
-      pkgs.xorg.fontbh100dpi
     ];
 
 in
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
index 5b8d5b214496b..b78e678488530 100644
--- a/nixos/modules/config/i18n.nix
+++ b/nixos/modules/config/i18n.nix
@@ -53,7 +53,23 @@ with lib;
 
       supportedLocales = mkOption {
         type = types.listOf types.str;
-        default = ["all"];
+        default = unique
+          (builtins.map (l: (replaceStrings [ "utf8" "utf-8" "UTF8" ] [ "UTF-8" "UTF-8" "UTF-8" ] l) + "/UTF-8") (
+            [
+              "C.UTF-8"
+              "en_US.UTF-8"
+              config.i18n.defaultLocale
+            ] ++ (attrValues (filterAttrs (n: v: n != "LANGUAGE") config.i18n.extraLocaleSettings))
+          ));
+        defaultText = literalExpression ''
+          unique
+            (builtins.map (l: (replaceStrings [ "utf8" "utf-8" "UTF8" ] [ "UTF-8" "UTF-8" "UTF-8" ] l) + "/UTF-8") (
+              [
+                "C.UTF-8"
+                config.i18n.defaultLocale
+              ] ++ (attrValues (filterAttrs (n: v: n != "LANGUAGE") config.i18n.extraLocaleSettings))
+            ))
+        '';
         example = ["en_US.UTF-8/UTF-8" "nl_NL.UTF-8/UTF-8" "nl_NL/ISO-8859-1"];
         description = ''
           List of locales that the system should support.  The value
diff --git a/nixos/modules/config/nsswitch.nix b/nixos/modules/config/nsswitch.nix
index 91a36cef10e67..e494ff5f74d5a 100644
--- a/nixos/modules/config/nsswitch.nix
+++ b/nixos/modules/config/nsswitch.nix
@@ -95,11 +95,14 @@ with lib;
   config = {
     assertions = [
       {
-        # Prevent users from disabling nscd, with nssModules being set.
-        # If disabling nscd is really necessary, it's still possible to opt out
-        # by forcing config.system.nssModules to [].
         assertion = config.system.nssModules.path != "" -> config.services.nscd.enable;
-        message = "Loading NSS modules from system.nssModules (${config.system.nssModules.path}), requires services.nscd.enable being set to true.";
+        message = ''
+          Loading NSS modules from system.nssModules (${config.system.nssModules.path}),
+          requires services.nscd.enable being set to true.
+
+          If disabling nscd is really necessary, it is possible to disable loading NSS modules
+          by setting `system.nssModules = lib.mkForce [];` in your configuration.nix.
+        '';
       }
     ];
 
diff --git a/nixos/modules/config/qt5.nix b/nixos/modules/config/qt5.nix
index eabba9ad95f00..9e19774b582fb 100644
--- a/nixos/modules/config/qt5.nix
+++ b/nixos/modules/config/qt5.nix
@@ -8,14 +8,21 @@ let
 
   isQGnome = cfg.platformTheme == "gnome" && builtins.elem cfg.style ["adwaita" "adwaita-dark"];
   isQtStyle = cfg.platformTheme == "gtk2" && !(builtins.elem cfg.style ["adwaita" "adwaita-dark"]);
+  isQt5ct = cfg.platformTheme == "qt5ct";
+  isLxqt = cfg.platformTheme == "lxqt";
+  isKde = cfg.platformTheme == "kde";
 
   packages = if isQGnome then [ pkgs.qgnomeplatform pkgs.adwaita-qt ]
     else if isQtStyle then [ pkgs.libsForQt5.qtstyleplugins ]
+    else if isQt5ct then [ pkgs.libsForQt5.qt5ct ]
+    else if isLxqt then [ pkgs.lxqt.lxqt-qtplugin pkgs.lxqt.lxqt-config ]
+    else if isKde then [ pkgs.libsForQt5.plasma-integration pkgs.libsForQt5.systemsettings ]
     else throw "`qt5.platformTheme` ${cfg.platformTheme} and `qt5.style` ${cfg.style} are not compatible.";
 
 in
 
 {
+  meta.maintainers = [ maintainers.romildo ];
 
   options = {
     qt5 = {
@@ -26,29 +33,29 @@ in
         type = types.enum [
           "gtk2"
           "gnome"
+          "lxqt"
+          "qt5ct"
+          "kde"
         ];
         example = "gnome";
         relatedPackages = [
           "qgnomeplatform"
           ["libsForQt5" "qtstyleplugins"]
+          ["libsForQt5" "qt5ct"]
+          ["lxqt" "lxqt-qtplugin"]
+          ["libsForQt5" "plasma-integration"]
         ];
-        description = ''
-          Selects the platform theme to use for Qt5 applications.</para>
-          <para>The options are
-          <variablelist>
-            <varlistentry>
-              <term><literal>gtk</literal></term>
-              <listitem><para>Use GTK theme with
-                <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link>
-              </para></listitem>
-            </varlistentry>
-            <varlistentry>
-              <term><literal>gnome</literal></term>
-              <listitem><para>Use GNOME theme with
-                <link xlink:href="https://github.com/FedoraQt/QGnomePlatform">qgnomeplatform</link>
-              </para></listitem>
-            </varlistentry>
-          </variablelist>
+        description = lib.mdDoc ''
+          Selects the platform theme to use for Qt5 applications.
+
+          The options are
+          - `gtk`: Use GTK theme with [qtstyleplugins](https://github.com/qt/qtstyleplugins)
+          - `gnome`: Use GNOME theme with [qgnomeplatform](https://github.com/FedoraQt/QGnomePlatform)
+          - `lxqt`: Use LXQt style set using the [lxqt-config-appearance](https://github.com/lxqt/lxqt-config)
+             application.
+          - `qt5ct`: Use Qt style set using the [qt5ct](https://sourceforge.net/projects/qt5ct/)
+             application.
+          - `kde`: Use Qt settings from Plasma.
         '';
       };
 
@@ -66,27 +73,14 @@ in
           "adwaita-qt"
           ["libsForQt5" "qtstyleplugins"]
         ];
-        description = ''
-          Selects the style to use for Qt5 applications.</para>
-          <para>The options are
-          <variablelist>
-            <varlistentry>
-              <term><literal>adwaita</literal></term>
-              <term><literal>adwaita-dark</literal></term>
-              <listitem><para>Use Adwaita Qt style with
-                <link xlink:href="https://github.com/FedoraQt/adwaita-qt">adwaita</link>
-              </para></listitem>
-            </varlistentry>
-            <varlistentry>
-              <term><literal>cleanlooks</literal></term>
-              <term><literal>gtk2</literal></term>
-              <term><literal>motif</literal></term>
-              <term><literal>plastique</literal></term>
-              <listitem><para>Use styles from
-                <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link>
-              </para></listitem>
-            </varlistentry>
-          </variablelist>
+        description = lib.mdDoc ''
+          Selects the style to use for Qt5 applications.
+
+          The options are
+          - `adwaita`, `adwaita-dark`: Use Adwaita Qt style with
+            [adwaita](https://github.com/FedoraQt/adwaita-qt)
+          - `cleanlooks`, `gtk2`, `motif`, `plastique`: Use styles from
+            [qtstyleplugins](https://github.com/qt/qtstyleplugins)
         '';
       };
     };
@@ -96,7 +90,7 @@ in
 
     environment.variables.QT_QPA_PLATFORMTHEME = cfg.platformTheme;
 
-    environment.variables.QT_STYLE_OVERRIDE = cfg.style;
+    environment.variables.QT_STYLE_OVERRIDE = mkIf (! (isQt5ct || isLxqt || isKde)) cfg.style;
 
     environment.systemPackages = packages;
 
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index cd0ed491383cf..3e14884bb2bff 100644
--- a/nixos/modules/config/resolvconf.nix
+++ b/nixos/modules/config/resolvconf.nix
@@ -47,10 +47,23 @@ in
 
       enable = mkOption {
         type = types.bool;
-        default = false;
-        internal = true;
+        default = !(config.environment.etc ? "resolv.conf");
+        defaultText = literalExpression ''!(config.environment.etc ? "resolv.conf")'';
+        description = ''
+          Whether DNS configuration is managed by resolvconf.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.openresolv;
+        defaultText = literalExpression "pkgs.openresolv";
         description = ''
-          DNS configuration is managed by resolvconf.
+          The package that provides the system-wide resolvconf command. Defaults to <literal>openresolv</literal>
+          if this module is enabled. Otherwise, can be used by other modules (for example <option>services.resolved</option>) to
+          provide a compatibility layer.
+
+          This option generally shouldn't be set by the user.
         '';
       };
 
@@ -110,8 +123,6 @@ in
 
   config = mkMerge [
     {
-      networking.resolvconf.enable = !(config.environment.etc ? "resolv.conf");
-
       environment.etc."resolvconf.conf".text =
         if !cfg.enable then
           # Force-stop any attempts to use resolvconf
@@ -121,10 +132,12 @@ in
             exit 1
           ''
         else configText;
+
+      environment.systemPackages = [ cfg.package ];
     }
 
     (mkIf cfg.enable {
-      environment.systemPackages = [ pkgs.openresolv ];
+      networking.resolvconf.package = pkgs.openresolv;
 
       systemd.services.resolvconf = {
         description = "resolvconf update";
@@ -136,7 +149,7 @@ in
 
         serviceConfig = {
           Type = "oneshot";
-          ExecStart = "${pkgs.openresolv}/bin/resolvconf -u";
+          ExecStart = "${cfg.package}/bin/resolvconf -u";
           RemainAfterExit = true;
         };
       };
diff --git a/nixos/modules/config/terminfo.nix b/nixos/modules/config/terminfo.nix
index 1396640af6724..693404a429c31 100644
--- a/nixos/modules/config/terminfo.nix
+++ b/nixos/modules/config/terminfo.nix
@@ -1,9 +1,33 @@
 # This module manages the terminfo database
 # and its integration in the system.
-{ config, ... }:
+{ config, lib, pkgs, ... }:
+
+with lib;
+
 {
+
+  options.environment.enableAllTerminfo = with lib; mkOption {
+    default = false;
+    type = types.bool;
+    description = ''
+      Whether to install all terminfo outputs
+    '';
+  };
+
   config = {
 
+    # can be generated with: filter (drv: (builtins.tryEval (drv ? terminfo)).value) (attrValues pkgs)
+    environment.systemPackages = mkIf config.environment.enableAllTerminfo (map (x: x.terminfo) (with pkgs; [
+      alacritty
+      foot
+      kitty
+      mtm
+      rxvt-unicode-unwrapped
+      rxvt-unicode-unwrapped-emoji
+      termite
+      wezterm
+    ]));
+
     environment.pathsToLink = [
       "/share/terminfo"
     ];
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 26ce561013b6f..5a21cb45d52be 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -223,10 +223,10 @@ foreach my $u (@{$spec->{users}}) {
     }
 
     # Ensure home directory incl. ownership and permissions.
-    if ($u->{createHome}) {
-        make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home} and ! $is_dry;
+    if ($u->{createHome} and !$is_dry) {
+        make_path($u->{home}, { mode => oct($u->{homeMode}) }) if ! -e $u->{home};
         chown $u->{uid}, $u->{gid}, $u->{home};
-        chmod 0700, $u->{home};
+        chmod oct($u->{homeMode}), $u->{home};
     }
 
     if (defined $u->{passwordFile}) {
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index b0f96c754fa53..85dfb93656195 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -6,12 +6,6 @@ let
   ids = config.ids;
   cfg = config.users;
 
-  isPasswdCompatible = str: !(hasInfix ":" str || hasInfix "\n" str);
-  passwdEntry = type: lib.types.addCheck type isPasswdCompatible // {
-    name = "passwdEntry ${type.name}";
-    description = "${type.description}, not containing newlines or colons";
-  };
-
   # Check whether a password hash will allow login.
   allowsLogin = hash:
     hash == "" # login without password
@@ -48,7 +42,7 @@ let
     services such as SSH, or indirectly via <command>su</command> or
     <command>sudo</command>). This should only be used for e.g. bootable
     live systems. Note: this is different from setting an empty password,
-    which ca be achieved using <option>users.users.&lt;name?&gt;.password</option>.
+    which can be achieved using <option>users.users.&lt;name?&gt;.password</option>.
 
     If set to <literal>null</literal> (default) this user will not
     be able to log in using a password (i.e. via <command>login</command>
@@ -60,7 +54,7 @@ let
     options = {
 
       name = mkOption {
-        type = passwdEntry types.str;
+        type = types.passwdEntry types.str;
         apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x;
         description = ''
           The name of the user account. If undefined, the name of the
@@ -69,7 +63,7 @@ let
       };
 
       description = mkOption {
-        type = passwdEntry types.str;
+        type = types.passwdEntry types.str;
         default = "";
         example = "Alice Q. User";
         description = ''
@@ -134,11 +128,17 @@ let
       };
 
       home = mkOption {
-        type = passwdEntry types.path;
+        type = types.passwdEntry types.path;
         default = "/var/empty";
         description = "The user's home directory.";
       };
 
+      homeMode = mkOption {
+        type = types.strMatching "[0-7]{1,5}";
+        default = "700";
+        description = "The user's home directory mode in numeric format. See chmod(1). The mode is only applied if <option>users.users.&lt;name&gt;.createHome</option> is true.";
+      };
+
       cryptHomeLuks = mkOption {
         type = with types; nullOr str;
         default = null;
@@ -163,7 +163,7 @@ let
       };
 
       shell = mkOption {
-        type = types.nullOr (types.either types.shellPackage (passwdEntry types.path));
+        type = types.nullOr (types.either types.shellPackage (types.passwdEntry types.path));
         default = pkgs.shadow;
         defaultText = literalExpression "pkgs.shadow";
         example = literalExpression "pkgs.bashInteractive";
@@ -319,6 +319,7 @@ let
           group = mkDefault "users";
           createHome = mkDefault true;
           home = mkDefault "/home/${config.name}";
+          homeMode = mkDefault "700";
           useDefaultShell = mkDefault true;
           isSystemUser = mkDefault false;
         })
@@ -342,7 +343,7 @@ let
     options = {
 
       name = mkOption {
-        type = passwdEntry types.str;
+        type = types.passwdEntry types.str;
         description = ''
           The name of the group. If undefined, the name of the attribute set
           will be used.
@@ -430,7 +431,7 @@ let
     inherit (cfg) mutableUsers;
     users = mapAttrsToList (_: u:
       { inherit (u)
-          name uid group description home createHome isSystemUser
+          name uid group description home homeMode createHome isSystemUser
           password passwordFile hashedPassword
           autoSubUidGidRange subUidRanges subGidRanges
           initialPassword initialHashedPassword;
diff --git a/nixos/modules/config/xdg/icons.nix b/nixos/modules/config/xdg/icons.nix
index c83fdc251ef00..1e91670cf03bf 100644
--- a/nixos/modules/config/xdg/icons.nix
+++ b/nixos/modules/config/xdg/icons.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 {
@@ -23,6 +23,12 @@ with lib;
       "/share/pixmaps"
     ];
 
+    environment.systemPackages = [
+      # Empty icon theme that contains index.theme file describing directories
+      # where toolkits should look for icons installed by apps.
+      pkgs.hicolor-icon-theme
+    ];
+
     # libXcursor looks for cursors in XCURSOR_PATH
     # it mostly follows the spec for icons
     # See: https://www.x.org/releases/current/doc/man/man3/Xcursor.3.xhtml Themes
diff --git a/nixos/modules/config/xdg/portal.nix b/nixos/modules/config/xdg/portal.nix
index 088f2af59e22a..1e6ddd7c4a26b 100644
--- a/nixos/modules/config/xdg/portal.nix
+++ b/nixos/modules/config/xdg/portal.nix
@@ -1,10 +1,30 @@
 { config, pkgs, lib, ... }:
 
-with lib;
+let
+  inherit (lib)
+    mkEnableOption
+    mkIf
+    mkOption
+    mkRenamedOptionModule
+    teams
+    types;
+in
 
 {
   imports = [
     (mkRenamedOptionModule [ "services" "flatpak" "extraPortals" ] [ "xdg" "portal" "extraPortals" ])
+
+    ({ config, lib, options, ... }:
+      let
+        from = [ "xdg" "portal" "gtkUsePortal" ];
+        fromOpt = lib.getAttrFromPath from options;
+      in
+      {
+        warnings = lib.mkIf config.xdg.portal.gtkUsePortal [
+          "The option `${lib.showOption from}' defined in ${lib.showFiles fromOpt.files} has been deprecated. Setting the variable globally with `environment.sessionVariables' NixOS option can have unforseen side-effects."
+        ];
+      }
+    )
   ];
 
   meta = {
@@ -32,11 +52,12 @@ with lib;
 
     gtkUsePortal = mkOption {
       type = types.bool;
+      visible = false;
       default = false;
       description = ''
         Sets environment variable <literal>GTK_USE_PORTAL</literal> to <literal>1</literal>.
-        This is needed for packages ran outside Flatpak to respect and use XDG Desktop Portals.
-        For example, you'd need to set this for non-flatpak Firefox to use native filechoosers.
+        This will force GTK-based programs ran outside Flatpak to respect and use XDG Desktop Portals
+        for features like file chooser but it is an unsupported hack that can easily break things.
         Defaults to <literal>false</literal> to respect its opt-in nature.
       '';
     };
diff --git a/nixos/modules/config/xdg/portals/lxqt.nix b/nixos/modules/config/xdg/portals/lxqt.nix
new file mode 100644
index 0000000000000..e85e2cc326933
--- /dev/null
+++ b/nixos/modules/config/xdg/portals/lxqt.nix
@@ -0,0 +1,49 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.xdg.portal.lxqt;
+
+in
+{
+  meta = {
+    maintainers = teams.lxqt.members;
+  };
+
+  options.xdg.portal.lxqt = {
+    enable = mkEnableOption ''
+      the desktop portal for the LXQt desktop environment.
+
+      This will add the <package>lxqt.xdg-desktop-portal-lxqt</package>
+      package (with the extra Qt styles) into the
+      <option>xdg.portal.extraPortals</option> option
+    '';
+
+    styles = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression ''[
+        pkgs.libsForQt5.qtstyleplugin-kvantum
+        pkgs.breeze-qt5
+        pkgs.qtcurve
+      ];
+      '';
+      description = ''
+        Extra Qt styles that will be available to the
+        <package>lxqt.xdg-desktop-portal-lxqt</package>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    xdg.portal = {
+      enable = true;
+      extraPortals = [
+        (pkgs.lxqt.xdg-desktop-portal-lxqt.override { extraQtStyles = cfg.styles; })
+      ];
+    };
+
+    environment.systemPackages = cfg.styles;
+  };
+}
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index 99bdb11b0112e..89a1217dfb313 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -27,7 +27,8 @@ in {
     };
 
     hardware.enableRedistributableFirmware = mkOption {
-      default = false;
+      default = config.hardware.enableAllFirmware;
+      defaultText = lib.literalExpression "config.hardware.enableAllFirmware";
       type = types.bool;
       description = ''
         Turn on this option if you want to enable all the firmware with a license allowing redistribution.
@@ -71,7 +72,7 @@ in {
     })
     (mkIf cfg.enableAllFirmware {
       assertions = [{
-        assertion = !cfg.enableAllFirmware || (config.nixpkgs.config.allowUnfree or false);
+        assertion = !cfg.enableAllFirmware || config.nixpkgs.config.allowUnfree;
         message = ''
           the list of hardware.enableAllFirmware contains non-redistributable licensed firmware files.
             This requires nixpkgs.config.allowUnfree to be true.
@@ -82,8 +83,11 @@ in {
         broadcom-bt-firmware
         b43Firmware_5_1_138
         b43Firmware_6_30_163_46
-        b43FirmwareCutter
-      ] ++ optional pkgs.stdenv.hostPlatform.isx86 facetimehd-firmware;
+        xow_dongle-firmware
+      ] ++ optionals pkgs.stdenv.hostPlatform.isx86 [
+        facetimehd-calibration
+        facetimehd-firmware
+      ];
     })
     (mkIf cfg.wirelessRegulatoryDatabase {
       hardware.firmware = [ pkgs.wireless-regdb ];
diff --git a/nixos/modules/hardware/ckb-next.nix b/nixos/modules/hardware/ckb-next.nix
index b2bbd77c9d7fa..751ed663aab57 100644
--- a/nixos/modules/hardware/ckb-next.nix
+++ b/nixos/modules/hardware/ckb-next.nix
@@ -48,6 +48,6 @@ in
     };
 
     meta = {
-      maintainers = with lib.maintainers; [ kierdavis ];
+      maintainers = with lib.maintainers; [ superherointj ];
     };
   }
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
index be67116ad507d..5a8a8e27bee1b 100644
--- a/nixos/modules/hardware/device-tree.nix
+++ b/nixos/modules/hardware/device-tree.nix
@@ -36,14 +36,11 @@ let
           /plugin/;
           / {
                   compatible = "raspberrypi";
-                  fragment@0 {
-                          target-path = "/soc";
-                          __overlay__ {
-                                  pps {
-                                          compatible = "pps-gpio";
-                                          status = "okay";
-                                  };
-                          };
+          };
+          &{/soc} {
+                  pps {
+                          compatible = "pps-gpio";
+                          status = "okay";
                   };
           };
         '';
@@ -88,13 +85,14 @@ let
 
   # Compile single Device Tree overlay source
   # file (.dts) into its compiled variant (.dtbo)
-  compileDTS = name: f: pkgs.callPackage({ dtc }: pkgs.stdenv.mkDerivation {
+  compileDTS = name: f: pkgs.callPackage({ stdenv, dtc }: stdenv.mkDerivation {
     name = "${name}-dtbo";
 
     nativeBuildInputs = [ dtc ];
 
     buildCommand = ''
-      dtc -I dts ${f} -O dtb -@ -o $out
+      $CC -E -nostdinc -I${getDev cfg.kernelPackage}/lib/modules/${cfg.kernelPackage.modDirVersion}/source/scripts/dtc/include-prefixes -undef -D__DTS__ -x assembler-with-cpp ${f} | \
+        dtc -I dts -O dtb -@ -o $out
     '';
   }) {};
 
diff --git a/nixos/modules/hardware/gpgsmartcards.nix b/nixos/modules/hardware/gpgsmartcards.nix
index 6e5fcda6b8519..4c1d0cc5b2a3f 100644
--- a/nixos/modules/hardware/gpgsmartcards.nix
+++ b/nixos/modules/hardware/gpgsmartcards.nix
@@ -19,7 +19,7 @@ let
   # per debian's udev deb hook (https://man7.org/linux/man-pages/man1/dh_installudev.1.html)
   destination = "60-scdaemon.rules";
 
-  scdaemonUdevRulesPkg = pkgs.runCommandNoCC "scdaemon-udev-rules" {} ''
+  scdaemonUdevRulesPkg = pkgs.runCommand "scdaemon-udev-rules" {} ''
     loc="$out/lib/udev/rules.d/"
     mkdir -p "''${loc}"
     cp "${scdaemonRules}" "''${loc}/${destination}"
diff --git a/nixos/modules/hardware/keyboard/uhk.nix b/nixos/modules/hardware/keyboard/uhk.nix
new file mode 100644
index 0000000000000..bf2d739c3a97e
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/uhk.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.keyboard.uhk;
+in
+{
+  options.hardware.keyboard.uhk = {
+    enable = mkEnableOption ''
+    non-root access to the firmware of UHK keyboards.
+      You need it when you want to flash a new firmware on the keyboard.
+      Access to the keyboard is granted to users in the "input" group.
+      You may want to install the uhk-agent package.
+    '';
+
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.uhk-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/network/b43.nix b/nixos/modules/hardware/network/b43.nix
index e63f2d04d1a6f..eb03bf223ccfe 100644
--- a/nixos/modules/hardware/network/b43.nix
+++ b/nixos/modules/hardware/network/b43.nix
@@ -24,10 +24,6 @@ let kernelVersion = config.boot.kernelPackages.kernel.version; in
   ###### implementation
 
   config = mkIf config.networking.enableB43Firmware {
-    assertions = singleton
-      { assertion = lessThan 0 (builtins.compareVersions kernelVersion "3.2");
-        message = "b43 firmware for kernels older than 3.2 not packaged yet!";
-      };
     hardware.firmware = [ pkgs.b43Firmware_5_1_138 ];
   };
 
diff --git a/nixos/modules/hardware/new-lg4ff.nix b/nixos/modules/hardware/new-lg4ff.nix
new file mode 100644
index 0000000000000..3c7f66f8d89b5
--- /dev/null
+++ b/nixos/modules/hardware/new-lg4ff.nix
@@ -0,0 +1,29 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.new-lg4ff;
+  kernelPackages = config.boot.kernelPackages;
+in {
+  options.hardware.new-lg4ff = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables improved Linux module drivers for Logitech driving wheels.
+        This will replace the existing in-kernel hid-logitech modules.
+        Works most notably on the Logitech G25, G27, G29 and Driving Force (GT).
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    boot = {
+      extraModulePackages = [ kernelPackages.new-lg4ff ];
+      kernelModules = [ "hid-logitech-new" ];
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ matthiasbenaets ];
+}
diff --git a/nixos/modules/hardware/raid/hpsa.nix b/nixos/modules/hardware/raid/hpsa.nix
index c4977e3fd70aa..120348a74bfbe 100644
--- a/nixos/modules/hardware/raid/hpsa.nix
+++ b/nixos/modules/hardware/raid/hpsa.nix
@@ -8,7 +8,10 @@ let
     version = "2.40-13.0";
 
     src = pkgs.fetchurl {
-      url = "https://downloads.linux.hpe.com/SDR/downloads/MCP/Ubuntu/pool/non-free/${pname}-${version}_amd64.deb";
+      urls = [
+        "https://downloads.linux.hpe.com/SDR/downloads/MCP/Ubuntu/pool/non-free/${pname}-${version}_amd64.deb"
+        "http://apt.netangels.net/pool/main/h/hpssacli/${pname}-${version}_amd64.deb"
+      ];
       sha256 = "11w7fwk93lmfw0yya4jpjwdmgjimqxx6412sqa166g1pz4jil4sw";
     };
 
@@ -37,7 +40,7 @@ let
       homepage = "https://downloads.linux.hpe.com/SDR/downloads/MCP/Ubuntu/pool/non-free/";
       license = licenses.unfreeRedistributable;
       platforms = [ "x86_64-linux" ];
-      maintainers = with maintainers; [ volth ];
+      maintainers = with maintainers; [ ];
     };
   };
 in {
diff --git a/nixos/modules/hardware/saleae-logic.nix b/nixos/modules/hardware/saleae-logic.nix
new file mode 100644
index 0000000000000..a3810d640c483
--- /dev/null
+++ b/nixos/modules/hardware/saleae-logic.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.saleae-logic;
+in
+{
+  options.hardware.saleae-logic = {
+    enable = lib.mkEnableOption "udev rules for Saleae Logic devices";
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.saleae-logic-2;
+      defaultText = lib.literalExpression "pkgs.saleae-logic-2";
+      description = ''
+        Saleae Logic package to use.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.udev.packages = [ cfg.package ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ chivay ];
+}
diff --git a/nixos/modules/hardware/video/amdgpu-pro.nix b/nixos/modules/hardware/video/amdgpu-pro.nix
index d784befc9b887..299a30b0629b1 100644
--- a/nixos/modules/hardware/video/amdgpu-pro.nix
+++ b/nixos/modules/hardware/video/amdgpu-pro.nix
@@ -51,9 +51,10 @@ in
       (isYes "KALLSYMS_ALL")
     ];
 
-    boot.initrd.extraUdevRulesCommands = ''
+    boot.initrd.extraUdevRulesCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       cp -v ${package}/etc/udev/rules.d/*.rules $out/
     '';
+    boot.initrd.services.udev.packages = [ package ];
 
     environment.systemPackages =
       [ package.vulkan ] ++
diff --git a/nixos/modules/hardware/video/capture/mwprocapture.nix b/nixos/modules/hardware/video/capture/mwprocapture.nix
index 61bab533edaf7..76cb4c6ee9bfe 100644
--- a/nixos/modules/hardware/video/capture/mwprocapture.nix
+++ b/nixos/modules/hardware/video/capture/mwprocapture.nix
@@ -16,11 +16,6 @@ in
 
   config = mkIf cfg.enable {
 
-    assertions = singleton {
-      assertion = versionAtLeast kernelPackages.kernel.version "3.2";
-      message = "Magewell Pro Capture family module is not supported for kernels older than 3.2";
-    };
-
     boot.kernelModules = [ "ProCapture" ];
 
     environment.systemPackages = [ kernelPackages.mwprocapture ];
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index c0ba60e49a731..f0f7b8e449a27 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -24,6 +24,7 @@ let
   primeEnabled = syncCfg.enable || offloadCfg.enable;
   nvidiaPersistencedEnabled =  cfg.nvidiaPersistenced;
   nvidiaSettings = cfg.nvidiaSettings;
+  busIDType = types.strMatching "([[:print:]]+[\:\@][0-9]{1,3}\:[0-9]{1,2}\:[0-9])?";
 in
 
 {
@@ -68,7 +69,7 @@ in
     };
 
     hardware.nvidia.prime.nvidiaBusId = mkOption {
-      type = types.str;
+      type = busIDType;
       default = "";
       example = "PCI:1:0:0";
       description = ''
@@ -78,7 +79,7 @@ in
     };
 
     hardware.nvidia.prime.intelBusId = mkOption {
-      type = types.str;
+      type = busIDType;
       default = "";
       example = "PCI:0:2:0";
       description = ''
@@ -88,7 +89,7 @@ in
     };
 
     hardware.nvidia.prime.amdgpuBusId = mkOption {
-      type = types.str;
+      type = busIDType;
       default = "";
       example = "PCI:4:0:0";
       description = ''
@@ -162,8 +163,19 @@ in
       '';
     };
 
+    hardware.nvidia.forceFullCompositionPipeline = lib.mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to force-enable the full composition pipeline.
+        This sometimes fixes screen tearing issues.
+        This has been reported to reduce the performance of some OpenGL applications and may produce issues in WebGL.
+        It also drastically increases the time the driver needs to clock down after load.
+      '';
+    };
+
     hardware.nvidia.package = lib.mkOption {
-      type = lib.types.package;
+      type = types.package;
       default = config.boot.kernelPackages.nvidiaPackages.stable;
       defaultText = literalExpression "config.boot.kernelPackages.nvidiaPackages.stable";
       description = ''
@@ -171,6 +183,14 @@ in
       '';
       example = literalExpression "config.boot.kernelPackages.nvidiaPackages.legacy_340";
     };
+
+    hardware.nvidia.open = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Whether to use the open source kernel module
+      '';
+    };
   };
 
   config = let
@@ -219,6 +239,11 @@ in
         );
         message = "Required files for driver based power management don't exist.";
       }
+
+      {
+        assertion = cfg.open -> (cfg.package ? open && cfg.package ? firmware);
+        message = "This version of NVIDIA driver does not provide a corresponding opensource kernel driver";
+      }
     ];
 
     # If Optimus/PRIME is enabled, we:
@@ -244,7 +269,7 @@ in
       modules = optional (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
       deviceSection = ''
         BusID "${igpuBusId}"
-        ${optionalString syncCfg.enable ''Option "AccelMethod" "none"''}
+        ${optionalString (syncCfg.enable && igpuDriver != "amdgpu") ''Option "AccelMethod" "none"''}
       '';
     } ++ singleton {
       name = "nvidia";
@@ -254,13 +279,18 @@ in
         ''
           BusID "${pCfg.nvidiaBusId}"
           ${optionalString syncCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
-          ${optionalString cfg.powerManagement.finegrained "Option \"NVreg_DynamicPowerManagement=0x02\""}
         '';
       screenSection =
         ''
           Option "RandRRotation" "on"
-          ${optionalString syncCfg.enable "Option \"AllowEmptyInitialConfiguration\""}
-        '';
+        '' + optionalString syncCfg.enable ''
+          Option "AllowEmptyInitialConfiguration"
+        '' + optionalString cfg.forceFullCompositionPipeline ''
+          Option         "metamodes" "nvidia-auto-select +0+0 {ForceFullCompositionPipeline=On}"
+          Option         "AllowIndirectGLXProtocol" "off"
+          Option         "TripleBuffer" "on"
+        ''
+        ;
     };
 
     services.xserver.serverLayoutSection = optionalString syncCfg.enable ''
@@ -269,9 +299,15 @@ in
       Option "AllowNVIDIAGPUScreens"
     '';
 
-    services.xserver.displayManager.setupCommands = optionalString syncCfg.enable ''
+    services.xserver.displayManager.setupCommands = let
+      sinkGpuProviderName = if igpuDriver == "amdgpu" then
+        # find the name of the provider if amdgpu
+        "`${pkgs.xorg.xrandr}/bin/xrandr --listproviders | ${pkgs.gnugrep}/bin/grep -i AMD | ${pkgs.gnused}/bin/sed -n 's/^.*name://p'`"
+      else
+        igpuDriver;
+    in optionalString syncCfg.enable ''
       # Added by nvidia configuration module for Optimus/PRIME.
-      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource ${igpuDriver} NVIDIA-0
+      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource "${sinkGpuProviderName}" NVIDIA-0
       ${pkgs.xorg.xrandr}/bin/xrandr --auto
     '';
 
@@ -283,10 +319,14 @@ in
     environment.etc."egl/egl_external_platform.d".source =
       "/run/opengl-driver/share/egl/egl_external_platform.d/";
 
-    hardware.opengl.package = mkIf (!offloadCfg.enable) nvidia_x11.out;
-    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_x11.lib32;
+    hardware.opengl.extraPackages = [
+      nvidia_x11.out
+      pkgs.nvidia-vaapi-driver
+    ];
+    hardware.opengl.extraPackages32 = [
+      nvidia_x11.lib32
+      pkgs.pkgsi686Linux.nvidia-vaapi-driver
+    ];
 
     environment.systemPackages = [ nvidia_x11.bin ]
       ++ optionals cfg.nvidiaSettings [ nvidia_x11.settings ]
@@ -337,7 +377,8 @@ in
       ++ optional (nvidia_x11.persistenced != null && config.virtualisation.docker.enableNvidia)
         "L+ /run/nvidia-docker/extras/bin/nvidia-persistenced - - - - ${nvidia_x11.persistenced}/origBin/nvidia-persistenced";
 
-    boot.extraModulePackages = [ nvidia_x11.bin ];
+    boot.extraModulePackages = if cfg.open then [ nvidia_x11.open ] else [ nvidia_x11.bin ];
+    hardware.firmware = lib.optional cfg.open nvidia_x11.firmware;
 
     # nvidia-uvm is required by CUDA applications.
     boot.kernelModules = [ "nvidia-uvm" ] ++
@@ -345,17 +386,19 @@ in
 
     # If requested enable modesetting via kernel parameter.
     boot.kernelParams = optional (offloadCfg.enable || cfg.modesetting.enable) "nvidia-drm.modeset=1"
-      ++ optional cfg.powerManagement.enable "nvidia.NVreg_PreserveVideoMemoryAllocations=1";
+      ++ optional cfg.powerManagement.enable "nvidia.NVreg_PreserveVideoMemoryAllocations=1"
+      ++ optional cfg.open "nvidia.NVreg_OpenRmEnableUnsupportedGpus=1";
 
     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", RUN+="${pkgs.runtimeShell} -c 'for i in $$(cat /proc/driver/nvidia/gpus/*/information | grep Minor | cut -d \  -f 4); do mknod -m 666 /dev/nvidia$${i} c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) $${i}; done'"
         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 ''
+        KERNEL=="nvidia_uvm", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-uvm-tools c $$(grep nvidia-uvm /proc/devices | cut -d \  -f 1) 1'"
+      '' + optionalString cfg.powerManagement.finegrained (
+      optionalString (versionOlder config.boot.kernelPackages.kernel.version "5.5") ''
         # Remove NVIDIA USB xHCI Host Controller devices, if present
         ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{remove}="1"
 
@@ -364,7 +407,7 @@ in
 
         # 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"
@@ -372,7 +415,7 @@ in
         # 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"
diff --git a/nixos/modules/hardware/video/webcam/facetimehd.nix b/nixos/modules/hardware/video/webcam/facetimehd.nix
index d311f600c3197..c48eac5e9c116 100644
--- a/nixos/modules/hardware/video/webcam/facetimehd.nix
+++ b/nixos/modules/hardware/video/webcam/facetimehd.nix
@@ -14,12 +14,19 @@ in
 
   options.hardware.facetimehd.enable = mkEnableOption "facetimehd kernel module";
 
-  config = mkIf cfg.enable {
+  options.hardware.facetimehd.withCalibration = mkOption {
+    default = false;
+    example = true;
+    type = types.bool;
+    description = ''
+      Whether to include sensor calibration files for facetimehd.
+      This makes colors look much better but is experimental, see
+      <link xlink:href="https://github.com/patjak/facetimehd/wiki/Extracting-the-sensor-calibration-files"/>
+      for details.
+    '';
+  };
 
-    assertions = singleton {
-      assertion = versionAtLeast kernelPackages.kernel.version "3.19";
-      message = "facetimehd is not supported for kernels older than 3.19";
-    };
+  config = mkIf cfg.enable {
 
     boot.kernelModules = [ "facetimehd" ];
 
@@ -27,7 +34,8 @@ in
 
     boot.extraModulePackages = [ kernelPackages.facetimehd ];
 
-    hardware.firmware = [ pkgs.facetimehd-firmware ];
+    hardware.firmware = [ pkgs.facetimehd-firmware ]
+      ++ optional cfg.withCalibration pkgs.facetimehd-calibration;
 
     # unload module during suspend/hibernate as it crashes the whole system
     powerManagement.powerDownCommands = ''
diff --git a/nixos/modules/hardware/xone.nix b/nixos/modules/hardware/xone.nix
new file mode 100644
index 0000000000000..89690d8c6fb10
--- /dev/null
+++ b/nixos/modules/hardware/xone.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.xone;
+in
+{
+  options.hardware.xone = {
+    enable = mkEnableOption "the xone driver for Xbox One and Xbobx Series X|S accessories";
+  };
+
+  config = mkIf cfg.enable {
+    boot = {
+      blacklistedKernelModules = [ "xpad" "mt76x2u" ];
+      extraModulePackages = with config.boot.kernelPackages; [ xone ];
+    };
+    hardware.firmware = [ pkgs.xow_dongle-firmware ];
+  };
+
+  meta = {
+    maintainers = with maintainers; [ rhysmdnz ];
+  };
+}
diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix
index 414aabbbaa730..9ef0285f7b933 100644
--- a/nixos/modules/i18n/input-method/fcitx5.nix
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -5,7 +5,9 @@ with lib;
 let
   im = config.i18n.inputMethod;
   cfg = im.fcitx5;
-  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; };
+  addons = cfg.addons ++ optional cfg.enableRimeData pkgs.rime-data;
+  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit addons; };
+  whetherRimeDataDir = any (p: p.pname == "fcitx5-rime") cfg.addons;
 in {
   options = {
     i18n.inputMethod.fcitx5 = {
@@ -17,22 +19,30 @@ in {
           Enabled Fcitx5 addons.
         '';
       };
+
+      enableRimeData = mkEnableOption "default rime-data with fcitx5-rime";
     };
   };
 
   config = mkIf (im.enabled == "fcitx5") {
     i18n.inputMethod.package = fcitx5Package;
 
-    environment.variables = {
-      GTK_IM_MODULE = "fcitx";
-      QT_IM_MODULE = "fcitx";
-      XMODIFIERS = "@im=fcitx";
-    };
+    environment = mkMerge [{
+      variables = {
+        GTK_IM_MODULE = "fcitx";
+        QT_IM_MODULE = "fcitx";
+        XMODIFIERS = "@im=fcitx";
+        QT_PLUGIN_PATH = [ "${fcitx5Package}/${pkgs.qt6.qtbase.qtPluginPrefix}" ];
+      };
+    }
+    (mkIf whetherRimeDataDir {
+      pathsToLink = [
+        "/share/rime-data"
+      ];
 
-    systemd.user.services.fcitx5-daemon = {
-      enable = true;
-      script = "${fcitx5Package}/bin/fcitx5";
-      wantedBy = [ "graphical-session.target" ];
-    };
+      variables =  {
+        NIX_RIME_DATA_DIR = "/run/current-system/sw/share/rime-data";
+      };
+    })];
   };
 }
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index c5b0cbc21502a..907f6451fce11 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -23,6 +23,8 @@ let
       Name=IBus
       Type=Application
       Exec=${ibusPackage}/bin/ibus-daemon --daemonize --xim ${impanel}
+      # GNOME will launch ibus using systemd
+      NotShowIn=GNOME;
     '';
   };
 in
@@ -67,7 +69,7 @@ in
     programs.dconf.packages = [ ibusPackage ];
 
     services.dbus.packages = [
-      ibusAutostart
+      ibusPackage
     ];
 
     environment.variables = {
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 92164d65e5335..2f91cd39881d8 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -39,7 +39,8 @@ in
         echo "unpacking the NixOS/Nixpkgs sources..."
         mkdir -p /nix/var/nix/profiles/per-user/root
         ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
-          -i ${channelSources} --quiet --option build-use-substitutes false
+          -i ${channelSources} --quiet --option build-use-substitutes false \
+          ${optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
         mkdir -m 0700 -p /root/.nix-defexpr
         ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
         mkdir -m 0755 -p /var/lib/nixos
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
index 618057618d0c3..3f92b779d60a2 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-base.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
@@ -46,5 +46,5 @@ with lib;
     done
   '';
 
-  system.stateVersion = mkDefault "18.03";
+  system.stateVersion = lib.mkDefault lib.trivial.release;
 }
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
index fa19daf132800..8c7bac6f6cc1c 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
@@ -35,22 +35,35 @@ with lib;
   # Enable sound in graphical iso's.
   hardware.pulseaudio.enable = true;
 
-  environment.systemPackages = [
+  # VM guest additions to improve host-guest interaction
+  services.spice-vdagentd.enable = true;
+  services.qemuGuest.enable = true;
+  virtualisation.vmware.guest.enable = true;
+  virtualisation.hypervGuest.enable = true;
+  services.xe-guest-utilities.enable = true;
+  # The VirtualBox guest additions rely on an out-of-tree kernel module
+  # which lags behind kernel releases, potentially causing broken builds.
+  virtualisation.virtualbox.guest.enable = false;
+
+  # Enable plymouth
+  boot.plymouth.enable = true;
+
+  environment.defaultPackages = with pkgs; [
     # Include gparted for partitioning disks.
-    pkgs.gparted
+    gparted
 
     # Include some editors.
-    pkgs.vim
-    pkgs.bvi # binary editor
-    pkgs.joe
+    vim
+    nano
 
     # Include some version control tools.
-    pkgs.git
+    git
+    rsync
 
     # Firefox for reading the manual.
-    pkgs.firefox
+    firefox
 
-    pkgs.glxinfo
+    glxinfo
   ];
 
 }
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
new file mode 100644
index 0000000000000..d015e10c11d81
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
@@ -0,0 +1,60 @@
+# This module defines a NixOS installation CD that contains GNOME.
+
+{ pkgs, ... }:
+
+{
+  imports = [ ./installation-cd-graphical-calamares.nix ];
+
+  isoImage.edition = "gnome";
+
+  services.xserver.desktopManager.gnome = {
+    # Add Firefox and other tools useful for installation to the launcher
+    favoriteAppsOverride = ''
+      [org.gnome.shell]
+      favorite-apps=[ 'firefox.desktop', 'nixos-manual.desktop', 'org.gnome.Console.desktop', 'org.gnome.Nautilus.desktop', 'gparted.desktop', 'io.calamares.calamares.desktop' ]
+    '';
+
+    # Override GNOME defaults to disable GNOME tour and disable suspend
+    extraGSettingsOverrides = ''
+      [org.gnome.shell]
+      welcome-dialog-last-shown-version='9999999999'
+      [org.gnome.desktop.session]
+      idle-delay=0
+      [org.gnome.settings-daemon.plugins.power]
+      sleep-inactive-ac-type='nothing'
+      sleep-inactive-battery-type='nothing'
+    '';
+
+    extraGSettingsOverridePackages = [ pkgs.gnome.gnome-settings-daemon ];
+
+    enable = true;
+  };
+
+  # Theme calamares with GNOME theme
+  qt5 = {
+    enable = true;
+    platformTheme = "gnome";
+  };
+
+  # Fix scaling for calamares on wayland
+  environment.variables = {
+    QT_QPA_PLATFORM = "$([[ $XDG_SESSION_TYPE = \"wayland\" ]] && echo \"wayland\")";
+  };
+
+  services.xserver.displayManager = {
+    gdm = {
+      enable = true;
+      # autoSuspend makes the machine automatically suspend after inactivity.
+      # It's possible someone could/try to ssh'd into the machine and obviously
+      # have issues because it's inactive.
+      # See:
+      # * https://github.com/NixOS/nixpkgs/pull/63790
+      # * https://gitlab.gnome.org/GNOME/gnome-control-center/issues/22
+      autoSuspend = false;
+    };
+    autoLogin = {
+      enable = true;
+      user = "nixos";
+    };
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma5.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma5.nix
new file mode 100644
index 0000000000000..a4c46d58c85a4
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma5.nix
@@ -0,0 +1,49 @@
+# This module defines a NixOS installation CD that contains X11 and
+# Plasma 5.
+
+{ pkgs, ... }:
+
+{
+  imports = [ ./installation-cd-graphical-calamares.nix ];
+
+  isoImage.edition = "plasma5";
+
+  services.xserver = {
+    desktopManager.plasma5 = {
+      enable = true;
+    };
+
+    # Automatically login as nixos.
+    displayManager = {
+      sddm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "nixos";
+      };
+    };
+  };
+
+  environment.systemPackages = with pkgs; [
+    # Graphical text editor
+    kate
+  ];
+
+  system.activationScripts.installerDesktop = let
+
+    # Comes from documentation.nix when xserver and nixos.enable are true.
+    manualDesktopFile = "/run/current-system/sw/share/applications/nixos-manual.desktop";
+
+    homeDir = "/home/nixos/";
+    desktopDir = homeDir + "Desktop/";
+
+  in ''
+    mkdir -p ${desktopDir}
+    chown nixos ${homeDir} ${desktopDir}
+
+    ln -sfT ${manualDesktopFile} ${desktopDir + "nixos-manual.desktop"}
+    ln -sfT ${pkgs.gparted}/share/applications/gparted.desktop ${desktopDir + "gparted.desktop"}
+    ln -sfT ${pkgs.konsole}/share/applications/org.kde.konsole.desktop ${desktopDir + "org.kde.konsole.desktop"}
+    ln -sfT ${pkgs.calamares-nixos}/share/applications/io.calamares.calamares.desktop ${desktopDir + "io.calamares.calamares.desktop"}
+  '';
+
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix
new file mode 100644
index 0000000000000..8a6d30d1801a1
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix
@@ -0,0 +1,20 @@
+# This module adds the calamares installer to the basic graphical NixOS
+# installation CD.
+
+{ pkgs, ... }:
+let
+  calamares-nixos-autostart = pkgs.makeAutostartItem { name = "io.calamares.calamares"; package = pkgs.calamares-nixos; };
+in
+{
+  imports = [ ./installation-cd-graphical-base.nix ];
+
+  environment.systemPackages = with pkgs; [
+    # Calamares for graphical installation
+    libsForQt5.kpmcore
+    calamares-nixos
+    calamares-nixos-autostart
+    calamares-nixos-extensions
+    # Needed for calamares QML module packagechooserq
+    libsForQt5.full
+  ];
+}
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 12ad8a4ae0046..573b31b439c2d 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -1,8 +1,6 @@
 # This module defines a NixOS installation CD that contains GNOME.
 
-{ lib, ... }:
-
-with lib;
+{ ... }:
 
 {
   imports = [ ./installation-cd-graphical-base.nix ];
@@ -10,10 +8,10 @@ with lib;
   isoImage.edition = "gnome";
 
   services.xserver.desktopManager.gnome = {
-    # Add firefox to favorite-apps
+    # Add Firefox and other tools useful for installation to the launcher
     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' ]
+      favorite-apps=[ 'firefox.desktop', 'nixos-manual.desktop', 'org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'gparted.desktop' ]
     '';
     enable = true;
   };
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
index 098c2b2870b04..5c7617c9f8c1a 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
@@ -1,9 +1,7 @@
 # This module defines a NixOS installation CD that contains X11 and
 # Plasma 5.
 
-{ config, lib, pkgs, ... }:
-
-with lib;
+{ pkgs, ... }:
 
 {
   imports = [ ./installation-cd-graphical-base.nix ];
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 3ff1b3d670e9f..d1ccc6c2072f7 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -91,29 +91,9 @@ let
     SERIAL 0 115200
     TIMEOUT ${builtins.toString syslinuxTimeout}
     UI vesamenu.c32
-    MENU TITLE NixOS
     MENU BACKGROUND /isolinux/background.png
-    MENU RESOLUTION 800 600
-    MENU CLEAR
-    MENU ROWS 6
-    MENU CMDLINEROW -4
-    MENU TIMEOUTROW -3
-    MENU TABMSGROW  -2
-    MENU HELPMSGROW -1
-    MENU HELPMSGENDROW -1
-    MENU MARGIN 0
-
-    #                                FG:AARRGGBB  BG:AARRGGBB   shadow
-    MENU COLOR BORDER       30;44      #00000000    #00000000   none
-    MENU COLOR SCREEN       37;40      #FF000000    #00E2E8FF   none
-    MENU COLOR TABMSG       31;40      #80000000    #00000000   none
-    MENU COLOR TIMEOUT      1;37;40    #FF000000    #00000000   none
-    MENU COLOR TIMEOUT_MSG  37;40      #FF000000    #00000000   none
-    MENU COLOR CMDMARK      1;36;40    #FF000000    #00000000   none
-    MENU COLOR CMDLINE      37;40      #FF000000    #00000000   none
-    MENU COLOR TITLE        1;36;44    #00000000    #00000000   none
-    MENU COLOR UNSEL        37;44      #FF000000    #00000000   none
-    MENU COLOR SEL          7;37;40    #FFFFFFFF    #FF5277C3   std
+
+    ${config.isoImage.syslinuxTheme}
 
     DEFAULT boot
 
@@ -389,10 +369,10 @@ let
     ${lib.optionalString (refindBinary != null) ''
     # GRUB apparently cannot do "chainloader" operations on "CD".
     if [ "\$root" != "cd0" ]; then
-      # Force root to be the FAT partition
-      # Otherwise it breaks rEFInd's boot
-      search --set=root --no-floppy --fs-uuid 1234-5678
       menuentry 'rEFInd' --class refind {
+        # Force root to be the FAT partition
+        # Otherwise it breaks rEFInd's boot
+        search --set=root --no-floppy --fs-uuid 1234-5678
         chainloader (\$root)/EFI/boot/${refindBinary}
       }
     fi
@@ -420,10 +400,8 @@ let
     #   dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i)
     ''
       mkdir ./contents && cd ./contents
-      cp -rp "${efiDir}"/EFI .
-      mkdir ./boot
-      cp -p "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}" \
-        "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}" ./boot/
+      mkdir -p ./EFI/boot
+      cp -rp "${efiDir}"/EFI/boot/{grub.cfg,*.efi} ./EFI/boot
 
       # Rewrite dates for everything in the FS
       find . -exec touch --date=2000-01-01 {} +
@@ -441,11 +419,11 @@ let
       faketime "2000-01-01 00:00:00" mkfs.vfat -i 12345678 -n EFIBOOT "$out"
 
       # Force a fixed order in mcopy for better determinism, and avoid file globbing
-      for d in $(find EFI boot -type d | sort); do
+      for d in $(find EFI -type d | sort); do
         faketime "2000-01-01 00:00:00" mmd -i "$out" "::/$d"
       done
 
-      for f in $(find EFI boot -type f | sort); do
+      for f in $(find EFI -type f | sort); do
         mcopy -pvm -i "$out" "$f" "::/$f"
       done
 
@@ -501,7 +479,7 @@ in
                 + 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 (isPower && is32bit && isBigEndian) "-Xbcj powerpc"
                 + lib.optionalString (isSparc) "-Xbcj sparc";
       description = ''
         Compression settings to use for the squashfs nix store.
@@ -601,6 +579,37 @@ in
       '';
     };
 
+    isoImage.syslinuxTheme = mkOption {
+      default = ''
+        MENU TITLE NixOS
+        MENU RESOLUTION 800 600
+        MENU CLEAR
+        MENU ROWS 6
+        MENU CMDLINEROW -4
+        MENU TIMEOUTROW -3
+        MENU TABMSGROW  -2
+        MENU HELPMSGROW -1
+        MENU HELPMSGENDROW -1
+        MENU MARGIN 0
+
+        #                                FG:AARRGGBB  BG:AARRGGBB   shadow
+        MENU COLOR BORDER       30;44      #00000000    #00000000   none
+        MENU COLOR SCREEN       37;40      #FF000000    #00E2E8FF   none
+        MENU COLOR TABMSG       31;40      #80000000    #00000000   none
+        MENU COLOR TIMEOUT      1;37;40    #FF000000    #00000000   none
+        MENU COLOR TIMEOUT_MSG  37;40      #FF000000    #00000000   none
+        MENU COLOR CMDMARK      1;36;40    #FF000000    #00000000   none
+        MENU COLOR CMDLINE      37;40      #FF000000    #00000000   none
+        MENU COLOR TITLE        1;36;44    #00000000    #00000000   none
+        MENU COLOR UNSEL        37;44      #FF000000    #00000000   none
+        MENU COLOR SEL          7;37;40    #FFFFFFFF    #FF5277C3   std
+      '';
+      type = types.str;
+      description = ''
+        The syslinux theme used for BIOS boot.
+      '';
+    };
+
     isoImage.appendToMenuLabel = mkOption {
       default = " Installer";
       example = " Live System";
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix b/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
deleted file mode 100644
index 054c8c74a76b1..0000000000000
--- a/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
+++ /dev/null
@@ -1,160 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  # A dummy /etc/nixos/configuration.nix in the booted CD that
-  # rebuilds the CD's configuration (and allows the configuration to
-  # be modified, of course, providing a true live CD).  Problem is
-  # that we don't really know how the CD was built - the Nix
-  # expression language doesn't allow us to query the expression being
-  # evaluated.  So we'll just hope for the best.
-  dummyConfiguration = pkgs.writeText "configuration.nix"
-    ''
-      { config, pkgs, ... }:
-
-      { # Add your own options below, e.g.:
-        #   services.openssh.enable = true;
-        nixpkgs.config.platform = pkgs.platforms.fuloong2f_n32;
-      }
-    '';
-
-
-  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
-
-  # A clue for the kernel loading
-  kernelParams = pkgs.writeText "kernel-params.txt" ''
-    Kernel Parameters:
-      init=/boot/init ${toString config.boot.kernelParams}
-  '';
-
-  # System wide nixpkgs config
-  nixpkgsUserConfig = pkgs.writeText "config.nix" ''
-    pkgs:
-    {
-      platform = pkgs.platforms.fuloong2f_n32;
-    }
-  '';
-
-in
-
-{
-  imports = [ ./system-tarball.nix ];
-
-  # Disable some other stuff we don't need.
-  security.sudo.enable = false;
-
-  # Include only the en_US locale.  This saves 75 MiB or so compared to
-  # the full glibcLocales package.
-  i18n.supportedLocales = ["en_US.UTF-8/UTF-8" "en_US/ISO-8859-1"];
-
-  # Include some utilities that are useful for installing or repairing
-  # the system.
-  environment.systemPackages =
-    [ pkgs.w3m # needed for the manual anyway
-      pkgs.testdisk # useful for repairing boot problems
-      pkgs.ms-sys # for writing Microsoft boot sectors / MBRs
-      pkgs.parted
-      pkgs.ddrescue
-      pkgs.ccrypt
-      pkgs.cryptsetup # needed for dm-crypt volumes
-
-      # Some networking tools.
-      pkgs.sshfs-fuse
-      pkgs.socat
-      pkgs.screen
-      pkgs.wpa_supplicant # !!! should use the wpa module
-
-      # Hardware-related tools.
-      pkgs.sdparm
-      pkgs.hdparm
-      pkgs.dmraid
-
-      # Tools to create / manipulate filesystems.
-      pkgs.ntfsprogs # for resizing NTFS partitions
-      pkgs.btrfs-progs
-      pkgs.jfsutils
-
-      # Some compression/archiver tools.
-      pkgs.unzip
-      pkgs.zip
-      pkgs.xz
-      pkgs.dar # disk archiver
-
-      # Some editors.
-      pkgs.nvi
-      pkgs.bvi # binary editor
-      pkgs.joe
-    ];
-
-  # The initrd has to contain any module that might be necessary for
-  # mounting the CD/DVD.
-  boot.initrd.availableKernelModules =
-    [ "vfat" "reiserfs" ];
-
-  boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_10;
-  boot.kernelParams = [ "console=tty1" ];
-
-  boot.postBootCommands =
-    ''
-      mkdir -p /mnt
-
-      cp ${dummyConfiguration} /etc/nixos/configuration.nix
-    '';
-
-  # Some more help text.
-  services.getty.helpLine =
-    ''
-
-      Log in as "root" with an empty password.  ${
-        if config.services.xserver.enable then
-          "Type `start xserver' to start\nthe graphical user interface."
-        else ""
-      }
-    '';
-
-  # Include the firmware for various wireless cards.
-  networking.enableRalinkFirmware = true;
-  networking.enableIntel2200BGFirmware = true;
-
-  # To speed up further installation of packages, include the complete stdenv
-  # in the Nix store of the tarball.
-  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ]
-    ++ [
-      {
-        object = config.system.build.bootStage2;
-        symlink = "/boot/init";
-      }
-      {
-        object = config.system.build.toplevel;
-        symlink = "/boot/system";
-      }
-    ];
-
-  tarball.contents = [
-    { source = kernelParams;
-      target = "/kernelparams.txt";
-    }
-    { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
-      target = "/boot/" + config.system.boot.loader.kernelFile;
-    }
-    { source = nixpkgsUserConfig;
-      target = "/root/.nixpkgs/config.nix";
-    }
-  ];
-
-  # Allow sshd to be started manually through "start sshd".  It should
-  # not be started by default on the installation CD because the
-  # default root password is empty.
-  services.openssh.enable = true;
-  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
-
-  boot.loader.grub.enable = false;
-  boot.loader.generationsDir.enable = false;
-  system.boot.loader.kernelFile = "vmlinux";
-
-  nixpkgs.config = {
-    platform = pkgs.platforms.fuloong2f_n32;
-  };
-}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
deleted file mode 100644
index 887bf60d0fbe4..0000000000000
--- a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
+++ /dev/null
@@ -1,89 +0,0 @@
-Let all the files in the system tarball sit in a directory served by NFS (the
-NFS root) like this in exportfs:
-  /home/pcroot    192.168.1.0/24(rw,no_root_squash,no_all_squash)
-
-Run "exportfs -a" after editing /etc/exportfs, for the nfs server to be aware
-of the changes.
-
-Use a tftp server serving the root of boot/ (from the system tarball).
-
-In order to have PXE boot, use the boot/dhcpd.conf-example file for your dhcpd
-server, as it will point your PXE clients to pxelinux.0 from the tftp server.
-Adapt the configuration to your network.
-
-Adapt the pxelinux configuration (boot/pxelinux.cfg/default) to set the path to
-your nfrroot. If you use ip=dhcp in the kernel, the nfs server ip will be taken
-from dhcp and so you don't have to specify it.
-
-The linux in bzImage includes network drivers for some usual cards.
-
-
-QEMU Testing
----------------
-
-You can test qemu pxe boot without having a DHCP server adapted, but having
-nfsroot, like this:
-  qemu-system-x86_64 -tftp /home/pcroot/boot -net nic -net user,bootfile=pxelinux.0 -boot n
-
-I don't know how to use NFS through the qemu '-net user' though.
-
-
-QEMU Testing with NFS root and bridged network
--------------------------------------------------
-
-This allows testing with qemu as any other host in your LAN.
-
-Testing with the real dhcpd server requires setting up a bridge and having a
-tap device.
-  tunctl -t tap0
-  brctl addbr br0
-  brctl addif br0 eth0
-  brctl addif tap0 eth0
-  ifconfig eth0 0.0.0.0 up
-  ifconfig tap0 0.0.0.0 up
-  ifconfig br0 up # With your ip configuration
-
-Then you can run qemu:
-  qemu-system-x86_64 -boot n -net tap,ifname=tap0,script=no -net nic,model=e1000
-
-
-Using the system-tarball-pc in a chroot
---------------------------------------------------
-
-Installation:
-  mkdir nixos-chroot && cd nixos-chroot
-  tar xf your-system-tarball.tar.xz
-  mkdir sys dev proc tmp root var run
-  mount --bind /sys sys
-  mount --bind /dev dev
-  mount --bind /proc proc
-
-Activate the system: look for a directory in nix/store similar to:
-    "/nix/store/y0d1lcj9fppli0hl3x0m0ba5g1ndjv2j-nixos-feb97bx-53f008"
-Having found it, activate that nixos system *twice*:
-  chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
-  chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
-
-This runs a 'hostname' command. Restore your old hostname with:
-  hostname OLDHOSTNAME
-
-Copy your system resolv.conf to the /etc/resolv.conf inside the chroot:
-  cp /etc/resolv.conf etc
-
-Then you can get an interactive shell in the nixos chroot. '*' means
-to run inside the chroot interactive shell
-  chroot . /bin/sh
-*  source /etc/profile
-
-Populate the nix database: that should be done in the init script if you
-had booted this nixos. Run:
-*  `grep local-cmds run/current-system/init`
-
-Then you can proceed normally subscribing to a nixos channel:
-  nix-channel --add https://nixos.org/channels/nixos-unstable
-  nix-channel --update
-
-Testing:
-  nix-env -i hello
-  which hello
-  hello
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc.nix b/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
deleted file mode 100644
index 674fb6c8a33c5..0000000000000
--- a/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
+++ /dev/null
@@ -1,163 +0,0 @@
-# This module contains the basic configuration for building a NixOS
-# tarball, that can directly boot, maybe using PXE or unpacking on a fs.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
-
-  # For PXE kernel loading
-  pxeconfig = pkgs.writeText "pxeconfig-default" ''
-    default menu.c32
-    prompt 0
-
-    label bootlocal
-      menu default
-      localboot 0
-      timeout 80
-      TOTALTIMEOUT 9000
-
-    label nixos
-      MENU LABEL ^NixOS using nfsroot
-      KERNEL bzImage
-      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 init=${config.system.build.toplevel}/init rw
-
-    label memtest
-      MENU LABEL ^${pkgs.memtest86.name}
-      KERNEL memtest
-  '';
-
-  dhcpdExampleConfig = pkgs.writeText "dhcpd.conf-example" ''
-    # Example configuration for booting PXE.
-    allow booting;
-    allow bootp;
-
-    # Adapt this to your network configuration.
-    option domain-name "local";
-    option subnet-mask 255.255.255.0;
-    option broadcast-address 192.168.1.255;
-    option domain-name-servers 192.168.1.1;
-    option routers 192.168.1.1;
-
-    # PXE-specific configuration directives...
-    # Some BIOS don't accept slashes for paths inside the tftp servers,
-    # and will report Access Violation if they see slashes.
-    filename "pxelinux.0";
-    # For the TFTP and NFS root server. Set the IP of your server.
-    next-server 192.168.1.34;
-
-    subnet 192.168.1.0 netmask 255.255.255.0 {
-      range 192.168.1.50 192.168.1.55;
-    }
-  '';
-
-  readme = ./system-tarball-pc-readme.txt;
-
-in
-
-{
-  imports =
-    [ ./system-tarball.nix
-
-      # Profiles of this basic installation.
-      ../../profiles/all-hardware.nix
-      ../../profiles/base.nix
-      ../../profiles/installation-device.nix
-    ];
-
-  # To speed up further installation of packages, include the complete stdenv
-  # in the Nix store of the tarball.
-  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ];
-
-  tarball.contents =
-    [ { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
-        target = "/boot/" + config.system.boot.loader.kernelFile;
-      }
-      { source = "${pkgs.syslinux}/share/syslinux/pxelinux.0";
-        target = "/boot/pxelinux.0";
-      }
-      { source = "${pkgs.syslinux}/share/syslinux/menu.c32";
-        target = "/boot/menu.c32";
-      }
-      { source = pxeconfig;
-        target = "/boot/pxelinux.cfg/default";
-      }
-      { source = readme;
-        target = "/readme.txt";
-      }
-      { source = dhcpdExampleConfig;
-        target = "/boot/dhcpd.conf-example";
-      }
-      { source = "${pkgs.memtest86}/memtest.bin";
-        # We can't leave '.bin', because pxelinux interprets this specially,
-        # and it would not load the image fine.
-        # http://forum.canardpc.com/threads/46464-0104-when-launched-via-pxe
-        target = "/boot/memtest";
-      }
-    ];
-
-  # Allow sshd to be started manually through "start sshd".  It should
-  # not be started by default on the installation CD because the
-  # default root password is empty.
-  services.openssh.enable = true;
-  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
-
-  # To be able to use the systemTarball to catch troubles.
-  boot.crashDump = {
-    enable = true;
-    kernelPackages = pkgs.linuxKernel.packages.linux_3_4;
-  };
-
-  # No grub for the tarball.
-  boot.loader.grub.enable = false;
-
-  /* fake entry, just to have a happy stage-1. Users
-     may boot without having stage-1 though */
-  fileSystems.fake =
-    { mountPoint = "/";
-      device = "/dev/something";
-    };
-
-  nixpkgs.config = {
-    packageOverrides = p: {
-      linux_3_4 = p.linux_3_4.override {
-        extraConfig = ''
-          # Enable drivers in kernel for most NICs.
-          E1000 y
-          # E1000E y
-          # ATH5K y
-          8139TOO y
-          NE2K_PCI y
-          ATL1 y
-          ATL1E y
-          ATL1C y
-          VORTEX y
-          VIA_RHINE y
-          R8169 y
-
-          # Enable nfs root boot
-          UNIX y # http://www.linux-mips.org/archives/linux-mips/2006-11/msg00113.html
-          IP_PNP y
-          IP_PNP_DHCP y
-          FSCACHE y
-          NFS_FS y
-          NFS_FSCACHE y
-          ROOT_NFS y
-
-          # Enable devtmpfs
-          DEVTMPFS y
-          DEVTMPFS_MOUNT y
-        '';
-      };
-    };
-  };
-}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix b/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
deleted file mode 100644
index 458e313a3f751..0000000000000
--- a/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
+++ /dev/null
@@ -1,172 +0,0 @@
-# This module contains the basic configuration for building a NixOS
-# tarball for the sheevaplug.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  # A dummy /etc/nixos/configuration.nix in the booted CD that
-  # rebuilds the CD's configuration (and allows the configuration to
-  # be modified, of course, providing a true live CD).  Problem is
-  # that we don't really know how the CD was built - the Nix
-  # expression language doesn't allow us to query the expression being
-  # evaluated.  So we'll just hope for the best.
-  dummyConfiguration = pkgs.writeText "configuration.nix"
-    ''
-      { config, pkgs, ... }:
-
-      {
-        # Add your own options below and run "nixos-rebuild switch".
-        # E.g.,
-        #   services.openssh.enable = true;
-      }
-    '';
-
-
-  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
-
-  # A clue for the kernel loading
-  kernelParams = pkgs.writeText "kernel-params.txt" ''
-    Kernel Parameters:
-      init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
-  '';
-
-
-in
-
-{
-  imports = [ ./system-tarball.nix ];
-
-  # Disable some other stuff we don't need.
-  security.sudo.enable = false;
-
-  # Include only the en_US locale.  This saves 75 MiB or so compared to
-  # the full glibcLocales package.
-  i18n.supportedLocales = ["en_US.UTF-8/UTF-8" "en_US/ISO-8859-1"];
-
-  # Include some utilities that are useful for installing or repairing
-  # the system.
-  environment.systemPackages =
-    [ pkgs.w3m # needed for the manual anyway
-      pkgs.ddrescue
-      pkgs.ccrypt
-      pkgs.cryptsetup # needed for dm-crypt volumes
-
-      # Some networking tools.
-      pkgs.sshfs-fuse
-      pkgs.socat
-      pkgs.screen
-      pkgs.wpa_supplicant # !!! should use the wpa module
-
-      # Hardware-related tools.
-      pkgs.sdparm
-      pkgs.hdparm
-      pkgs.dmraid
-
-      # Tools to create / manipulate filesystems.
-      pkgs.btrfs-progs
-
-      # Some compression/archiver tools.
-      pkgs.unzip
-      pkgs.zip
-      pkgs.xz
-      pkgs.dar # disk archiver
-
-      # Some editors.
-      pkgs.nvi
-      pkgs.bvi # binary editor
-      pkgs.joe
-    ];
-
-  boot.loader.grub.enable = false;
-  boot.loader.generationsDir.enable = false;
-  system.boot.loader.kernelFile = "uImage";
-
-  boot.initrd.availableKernelModules =
-    [ "mvsdio" "reiserfs" "ext3" "ums-cypress" "rtc_mv" "ext4" ];
-
-  boot.postBootCommands =
-    ''
-      mkdir -p /mnt
-
-      cp ${dummyConfiguration} /etc/nixos/configuration.nix
-    '';
-
-  boot.initrd.extraUtilsCommands =
-    ''
-      copy_bin_and_libs ${pkgs.util-linux}/sbin/hwclock
-    '';
-
-  boot.initrd.postDeviceCommands =
-    ''
-      hwclock -s
-    '';
-
-  boot.kernelParams =
-    [
-      "selinux=0"
-      "console=tty1"
-      # "console=ttyS0,115200n8"  # serial console
-    ];
-
-  boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_4;
-
-  boot.supportedFilesystems = [ "reiserfs" ];
-
-  /* fake entry, just to have a happy stage-1. Users
-     may boot without having stage-1 though */
-  fileSystems.fake =
-    { mountPoint = "/";
-      device = "/dev/something";
-    };
-
-  services.getty = {
-    # Some more help text.
-    helpLine = ''
-      Log in as "root" with an empty password.  ${
-        if config.services.xserver.enable then
-          "Type `start xserver' to start\nthe graphical user interface."
-        else ""
-      }
-    '';
-  };
-
-  # Setting vesa, we don't get the nvidia driver, which can't work in arm.
-  services.xserver.videoDrivers = [ "vesa" ];
-
-  documentation.nixos.enable = false;
-
-  # Include the firmware for various wireless cards.
-  networking.enableRalinkFirmware = true;
-  networking.enableIntel2200BGFirmware = true;
-
-  # To speed up further installation of packages, include the complete stdenv
-  # in the Nix store of the tarball.
-  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ];
-  tarball.contents = [
-    { source = kernelParams;
-      target = "/kernelparams.txt";
-    }
-    { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
-      target = "/boot/" + config.system.boot.loader.kernelFile;
-    }
-    { source = pkgs.ubootSheevaplug;
-      target = "/boot/uboot";
-    }
-  ];
-
-  # Allow sshd to be started manually through "start sshd".  It should
-  # not be started by default on the installation CD because the
-  # default root password is empty.
-  services.openssh.enable = true;
-  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
-
-  # cpufrequtils fails to build on non-pc
-  powerManagement.enable = false;
-
-  nixpkgs.config = {
-    platform = pkgs.platforms.sheevaplug;
-  };
-}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball.nix b/nixos/modules/installer/cd-dvd/system-tarball.nix
deleted file mode 100644
index 362c555cc53e8..0000000000000
--- a/nixos/modules/installer/cd-dvd/system-tarball.nix
+++ /dev/null
@@ -1,93 +0,0 @@
-# This module creates a bootable ISO image containing the given NixOS
-# configuration.  The derivation for the ISO image will be placed in
-# config.system.build.tarball.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  versionFile = pkgs.writeText "nixos-label" config.system.nixos.label;
-
-in
-
-{
-  options = {
-    tarball.contents = mkOption {
-      example = literalExpression ''
-        [ { source = pkgs.memtest86 + "/memtest.bin";
-            target = "boot/memtest.bin";
-          }
-        ]
-      '';
-      description = ''
-        This option lists files to be copied to fixed locations in the
-        generated ISO image.
-      '';
-    };
-
-    tarball.storeContents = mkOption {
-      example = literalExpression "[ pkgs.stdenv ]";
-      description = ''
-        This option lists additional derivations to be included in the
-        Nix store in the generated ISO image.
-      '';
-    };
-
-  };
-
-  config = {
-
-    # In stage 1 of the boot, mount the CD/DVD as the root FS by label
-    # so that we don't need to know its device.
-    fileSystems = { };
-
-    # boot.initrd.availableKernelModules = [ "mvsdio" "reiserfs" "ext3" "ext4" ];
-
-    # boot.initrd.kernelModules = [ "rtc_mv" ];
-
-    # Closures to be copied to the Nix store on the CD, namely the init
-    # script and the top-level system configuration directory.
-    tarball.storeContents =
-      [ { object = config.system.build.toplevel;
-          symlink = "/run/current-system";
-        }
-      ];
-
-    # Individual files to be included on the CD, outside of the Nix
-    # store on the CD.
-    tarball.contents =
-      [ { source = config.system.build.initialRamdisk + "/" + config.system.boot.loader.initrdFile;
-          target = "/boot/" + config.system.boot.loader.initrdFile;
-        }
-        { source = versionFile;
-          target = "/nixos-version.txt";
-        }
-      ];
-
-    # Create the tarball
-    system.build.tarball = import ../../../lib/make-system-tarball.nix {
-      inherit (pkgs) stdenv closureInfo pixz;
-
-      inherit (config.tarball) contents storeContents;
-    };
-
-    boot.postBootCommands =
-      ''
-        # After booting, register the contents of the Nix store on the
-        # CD in the Nix database in the tmpfs.
-        if [ -f /nix-path-registration ]; then
-          ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
-          rm /nix-path-registration
-        fi
-
-        # nixos-rebuild also requires a "system" profile and an
-        # /etc/NIXOS tag.
-        touch /etc/NIXOS
-        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
-      '';
-
-  };
-
-}
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
index a459e7304cd41..fed6a7c372879 100644
--- a/nixos/modules/installer/netboot/netboot.nix
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -81,7 +81,7 @@ with lib;
 
 
     # Create the initrd
-    system.build.netbootRamdisk = pkgs.makeInitrd {
+    system.build.netbootRamdisk = pkgs.makeInitrdNG {
       inherit (config.boot.initrd) compressor;
       prepend = [ "${config.system.build.initialRamdisk}/initrd" ];
 
@@ -101,6 +101,37 @@ with lib;
       boot
     '';
 
+    # A script invoking kexec on ./bzImage and ./initrd.gz.
+    # Usually used through system.build.kexecTree, but exposed here for composability.
+    system.build.kexecScript = pkgs.writeScript "kexec-boot" ''
+      #!/usr/bin/env bash
+      if ! kexec -v >/dev/null 2>&1; then
+        echo "kexec not found: please install kexec-tools" 2>&1
+        exit 1
+      fi
+      SCRIPT_DIR=$( cd -- "$( dirname -- "''${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+      kexec --load ''${SCRIPT_DIR}/bzImage \
+        --initrd=''${SCRIPT_DIR}/initrd.gz \
+        --command-line "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"
+      kexec -e
+    '';
+
+    # A tree containing initrd.gz, bzImage and a kexec-boot script.
+    system.build.kexecTree = pkgs.linkFarm "kexec-tree" [
+      {
+        name = "initrd.gz";
+        path = "${config.system.build.netbootRamdisk}/initrd";
+      }
+      {
+        name = "bzImage";
+        path = "${config.system.build.kernel}/${config.system.boot.loader.kernelFile}";
+      }
+      {
+        name = "kexec-boot";
+        path = config.system.build.kexecScript;
+      }
+    ];
+
     boot.loader.timeout = 10;
 
     boot.postBootCommands =
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64.nix b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
index 165e2aac27b49..cf01005fdc8a9 100644
--- a/nixos/modules/installer/sd-card/sd-image-aarch64.nix
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
@@ -24,6 +24,9 @@
         [pi3]
         kernel=u-boot-rpi3.bin
 
+        [pi02]
+        kernel=u-boot-rpi3.bin
+
         [pi4]
         kernel=u-boot-rpi4.bin
         enable_gic=1
@@ -33,6 +36,15 @@
         # what the pi3 firmware does by default.
         disable_overscan=1
 
+        # Supported in newer board revisions
+        arm_boost=1
+
+        [cm4]
+        # Enable host mode on the 2711 built-in XHCI USB controller.
+        # This line should be removed if the legacy DWC2 controller is required
+        # (e.g. for USB device mode) or if USB support is not required.
+        otg_mode=1
+
         [all]
         # Boot in 64-bit mode.
         arm_64bit=1
@@ -59,6 +71,9 @@
         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/
+        cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-400.dtb firmware/
+        cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-cm4.dtb firmware/
+        cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-cm4s.dtb firmware/
       '';
     populateRootCommands = ''
       mkdir -p ./files/boot
diff --git a/nixos/modules/installer/sd-card/sd-image.nix b/nixos/modules/installer/sd-card/sd-image.nix
index 7560c682517aa..c0335ea487592 100644
--- a/nixos/modules/installer/sd-card/sd-image.nix
+++ b/nixos/modules/installer/sd-card/sd-image.nix
@@ -18,7 +18,7 @@ with lib;
 let
   rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
     inherit (config.sdImage) storePaths;
-    compressImage = true;
+    compressImage = config.sdImage.compressImage;
     populateImageCommands = config.sdImage.populateRootCommands;
     volumeLabel = "NIXOS_SD";
   } // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
@@ -174,7 +174,8 @@ in
     mtools, libfaketime, util-linux, zstd }: stdenv.mkDerivation {
       name = config.sdImage.imageName;
 
-      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime util-linux zstd ];
+      nativeBuildInputs = [ dosfstools e2fsprogs libfaketime mtools util-linux ]
+      ++ lib.optional config.sdImage.compressImage zstd;
 
       inherit (config.sdImage) imageName compressImage;
 
@@ -189,14 +190,18 @@ in
           echo "file sd-image $img" >> $out/nix-support/hydra-build-products
         fi
 
+        root_fs=${rootfsImage}
+        ${lib.optionalString config.sdImage.compressImage ''
+        root_fs=./root-fs.img
         echo "Decompressing rootfs image"
-        zstd -d --no-progress "${rootfsImage}" -o ./root-fs.img
+        zstd -d --no-progress "${rootfsImage}" -o $root_fs
+        ''}
 
         # Gap in front of the first partition, in MiB
         gap=${toString config.sdImage.firmwarePartitionOffset}
 
         # Create the image file sized to fit /boot/firmware and /, plus slack for the gap.
-        rootSizeBlocks=$(du -B 512 --apparent-size ./root-fs.img | awk '{ print $1 }')
+        rootSizeBlocks=$(du -B 512 --apparent-size $root_fs | awk '{ print $1 }')
         firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512))
         imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024))
         truncate -s $imageSize $img
@@ -214,7 +219,7 @@ in
 
         # Copy the rootfs into the SD image
         eval $(partx $img -o START,SECTORS --nr 2 --pairs)
-        dd conv=notrunc if=./root-fs.img of=$img seek=$START count=$SECTORS
+        dd conv=notrunc if=$root_fs of=$img seek=$START count=$SECTORS
 
         # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
         eval $(partx $img -o START,SECTORS --nr 1 --pairs)
diff --git a/nixos/modules/installer/tools/get-version-suffix b/nixos/modules/installer/tools/get-version-suffix
index b8972cd57d22b..8d72905cdcb47 100644
--- a/nixos/modules/installer/tools/get-version-suffix
+++ b/nixos/modules/installer/tools/get-version-suffix
@@ -1,14 +1,15 @@
 getVersion() {
     local dir="$1"
     rev=
-    if [ -e "$dir/.git" ]; then
+    gitDir="$dir/.git"
+    if [ -e "$gitDir" ]; then
         if [ -z "$(type -P git)" ]; then
             echo "warning: Git not found; cannot figure out revision of $dir" >&2
             return
         fi
         cd "$dir"
-        rev=$(git rev-parse --short HEAD)
-        if git describe --always --dirty | grep -q dirty; then
+        rev=$(git --git-dir="$gitDir" rev-parse --short HEAD)
+        if git --git-dir="$gitDir" describe --always --dirty | grep -q dirty; then
             rev+=M
         fi
     fi
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index 31aeaad80d608..0035ceca6fc9c 100644
--- a/nixos/modules/installer/tools/nix-fallback-paths.nix
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -1,7 +1,7 @@
 {
-  x86_64-linux = "/nix/store/67amfijcvhqfgz4bwf2izsvbnklwjbvk-nix-2.6.0";
-  i686-linux = "/nix/store/kinl99f619b2xsma4qnzhidbp65axyzm-nix-2.6.0";
-  aarch64-linux = "/nix/store/8zpm63nn7k4n1alp9a0fcilpgc8j014z-nix-2.6.0";
-  x86_64-darwin = "/nix/store/hw5v03wnc0k1pwgiyhblwlxb1fx5zyx8-nix-2.6.0";
-  aarch64-darwin = "/nix/store/669p1vjnzi56fib98qczwlaglcwcnip4-nix-2.6.0";
+  x86_64-linux = "/nix/store/3af6g226v4hsv6x7xzh23d6wqyq0nzjp-nix-2.10.3";
+  i686-linux = "/nix/store/43xxh2jip6rpdhylc5z9a5fxx54dw206-nix-2.10.3";
+  aarch64-linux = "/nix/store/6qw3r57nra08ars8j8zyj3fl8lz4cvnd-nix-2.10.3";
+  x86_64-darwin = "/nix/store/3b7qrm0qjw57fmznrsvm0ai568i89hc2-nix-2.10.3";
+  aarch64-darwin = "/nix/store/gp7k17iy1n7hgf97qwnxw28c6v9nhb1i-nix-2.10.3";
 }
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 8aedce2fb49ce..b4a94f62ad939 100644
--- a/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
+++ b/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
@@ -25,4 +25,7 @@ pkgs.runCommand "nixos-build-vms" { nativeBuildInputs = [ pkgs.makeWrapper ]; }
   ln -s ${interactiveDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
   wrapProgram $out/bin/nixos-test-driver \
     --add-flags "--interactive"
+  wrapProgram $out/bin/nixos-run-vms \
+     --set testScript "${pkgs.writeText "start-all" "start_all(); join_all();"}" \
+     --add-flags "--no-interactive"
 ''
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
index 115b3d7a7c5ee..89beeee7cf9e5 100644
--- a/nixos/modules/installer/tools/nixos-enter.sh
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -63,32 +63,32 @@ mount --rbind /sys "$mountPoint/sys"
 
 # modified from https://github.com/archlinux/arch-install-scripts/blob/bb04ab435a5a89cd5e5ee821783477bc80db797f/arch-chroot.in#L26-L52
 chroot_add_resolv_conf() {
-    local chrootdir=$1 resolv_conf=$1/etc/resolv.conf
+    local chrootDir="$1" resolvConf="$1/etc/resolv.conf"
 
     [[ -e /etc/resolv.conf ]] || return 0
 
     # Handle resolv.conf as a symlink to somewhere else.
-    if [[ -L $chrootdir/etc/resolv.conf ]]; then
+    if [[ -L "$resolvConf" ]]; then
       # readlink(1) should always give us *something* since we know at this point
       # it's a symlink. For simplicity, ignore the case of nested symlinks.
-      # We also ignore the possibility if `../`s escaping the root.
-      resolv_conf=$(readlink "$chrootdir/etc/resolv.conf")
-      if [[ $resolv_conf = /* ]]; then
-        resolv_conf=$chrootdir$resolv_conf
+      # We also ignore the possibility of `../`s escaping the root.
+      resolvConf="$(readlink "$resolvConf")"
+      if [[ "$resolvConf" = /* ]]; then
+        resolvConf="$chrootDir$resolvConf"
       else
-        resolv_conf=$chrootdir/etc/$resolv_conf
+        resolvConf="$chrootDir/etc/$resolvConf"
       fi
     fi
 
     # ensure file exists to bind mount over
-    if [[ ! -f $resolv_conf ]]; then
-      install -Dm644 /dev/null "$resolv_conf" || return 1
+    if [[ ! -f "$resolvConf" ]]; then
+      install -Dm644 /dev/null "$resolvConf" || return 1
     fi
 
-    mount --bind /etc/resolv.conf "$resolv_conf"
+    mount --bind /etc/resolv.conf "$resolvConf"
 }
 
-chroot_add_resolv_conf "$mountPoint" || print "ERROR: failed to set up resolv.conf"
+chroot_add_resolv_conf "$mountPoint" || echo "$0: failed to set up resolv.conf" >&2
 
 (
     # If silent, write both stdout and stderr of activation script to /dev/null
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 57aef50a0f6ba..212b2b3cd23a2 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -51,7 +51,9 @@ for (my $n = 0; $n < scalar @ARGV; $n++) {
         $n++;
         $rootDir = $ARGV[$n];
         die "$0: ‘--root’ requires an argument\n" unless defined $rootDir;
+        die "$0: no need to specify `/` with `--root`, it is the default\n" if $rootDir eq "/";
         $rootDir =~ s/\/*$//; # remove trailing slashes
+        $rootDir = File::Spec->rel2abs($rootDir); # resolve absolute path
     }
     elsif ($arg eq "--force") {
         $force = 1;
@@ -82,6 +84,15 @@ sub debug {
 }
 
 
+# nixpkgs.system
+my ($status, @systemLines) = runCommand("@nixInstantiate@ --impure --eval --expr builtins.currentSystem");
+if ($status != 0 || join("", @systemLines) =~ /error/) {
+    die "Failed to retrieve current system type from nix.\n";
+}
+chomp(my $system = @systemLines[0]);
+push @attrs, "nixpkgs.hostPlatform = lib.mkDefault $system;";
+
+
 my $cpuinfo = read_file "/proc/cpuinfo";
 
 
@@ -289,6 +300,12 @@ if ($virt eq "oracle") {
     push @attrs, "virtualisation.virtualbox.guest.enable = true;"
 }
 
+# Check if we're a Parallels guest. If so, enable the guest additions.
+# It is blocked by https://github.com/systemd/systemd/pull/23859
+if ($virt eq "parallels") {
+    push @attrs, "hardware.parallels.enable = true;";
+    push @attrs, "nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ \"prl-tools\" ];";
+}
 
 # Likewise for QEMU.
 if ($virt eq "qemu" || $virt eq "kvm" || $virt eq "bochs") {
@@ -579,17 +596,19 @@ ${\join "", (map { "  $_\n" } (uniq @attrs))}}
 EOF
 
 sub generateNetworkingDhcpConfig {
+    # FIXME disable networking.useDHCP by default when switching to networkd.
     my $config = <<EOF;
-  # The global useDHCP flag is deprecated, therefore explicitly set to false here.
-  # Per-interface useDHCP will be mandatory in the future, so this generated config
-  # replicates the default behaviour.
-  networking.useDHCP = lib.mkDefault false;
+  # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
+  # (the default) this is the recommended approach. When using systemd-networkd it's
+  # still possible to use this option, but it's recommended to use it in conjunction
+  # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
+  networking.useDHCP = lib.mkDefault true;
 EOF
 
     foreach my $path (glob "/sys/class/net/*") {
         my $dev = basename($path);
         if ($dev ne "lo") {
-            $config .= "  networking.interfaces.$dev.useDHCP = lib.mkDefault true;\n";
+            $config .= "  # networking.interfaces.$dev.useDHCP = lib.mkDefault true;\n";
         }
     }
 
@@ -616,7 +635,12 @@ EOF
 if ($showHardwareConfig) {
     print STDOUT $hwConfig;
 } else {
-    $outDir = "$rootDir$outDir";
+    if ($outDir eq "/etc/nixos") {
+        $outDir = "$rootDir$outDir";
+    } else {
+        $outDir = File::Spec->rel2abs($outDir);
+        $outDir =~ s/\/*$//; # remove trailing slashes
+    }
 
     my $fn = "$outDir/hardware-configuration.nix";
     print STDERR "writing $fn...\n";
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 71aaf7f253d9b..8719a509d64c1 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -34,7 +34,8 @@ let
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
     perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
-    detectvirt = "${pkgs.systemd}/bin/systemd-detect-virt";
+    nixInstantiate = "${pkgs.nix}/bin/nix-instantiate";
+    detectvirt = "${config.systemd.package}/bin/systemd-detect-virt";
     btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
     inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
     xserverEnabled = config.services.xserver.enable;
@@ -117,7 +118,7 @@ in
     '';
   };
 
-  config = lib.mkIf (!config.system.disableInstallerTools) {
+  config = lib.mkIf (config.nix.enable && !config.system.disableInstallerTools) {
 
     system.nixos-generate-config.configuration = mkDefault ''
       # Edit this configuration file to define what should be installed on
@@ -177,6 +178,10 @@ in
         # users.users.jane = {
         #   isNormalUser = true;
         #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
+        #   packages = with pkgs; [
+        #     firefox
+        #     thunderbird
+        #   ];
         # };
 
         # List packages installed in system profile. To search, run:
@@ -184,7 +189,6 @@ in
         # environment.systemPackages = with pkgs; [
         #   vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
         #   wget
-        #   firefox
         # ];
 
         # Some programs need SUID wrappers, can be configured further or are
@@ -206,6 +210,11 @@ in
         # Or disable the firewall altogether.
         # networking.firewall.enable = false;
 
+        # Copy the NixOS configuration file and link it from the resulting system
+        # (/run/current-system/configuration.nix). This is useful in case you
+        # accidentally delete configuration.nix.
+        # system.copySystemConfiguration = true;
+
         # 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/documentation.nix b/nixos/modules/misc/documentation.nix
index b7746ddc21151..b031ff2f2be29 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -64,7 +64,8 @@ let
         filter =
           builtins.filterSource
             (n: t:
-              (t == "directory" -> baseNameOf n != "tests")
+              cleanSourceFilter n t
+              && (t == "directory" -> baseNameOf n != "tests")
               && (t == "file" -> hasSuffix ".nix" n)
             );
       in
@@ -129,7 +130,7 @@ let
       genericName = "View NixOS documentation in a web browser";
       icon = "nix-snowflake";
       exec = "nixos-help";
-      categories = "System";
+      categories = ["System"];
     };
 
     in pkgs.symlinkJoin {
@@ -177,19 +178,12 @@ in
       man.generateCaches = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = mdDoc ''
           Whether to generate the manual page index caches.
           This allows searching for a page or
-          keyword using utilities like
-          <citerefentry>
-            <refentrytitle>apropos</refentrytitle>
-            <manvolnum>1</manvolnum>
-          </citerefentry>
-          and the <literal>-k</literal> option of
-          <citerefentry>
-            <refentrytitle>man</refentrytitle>
-            <manvolnum>1</manvolnum>
-          </citerefentry>.
+          keyword using utilities like {manpage}`apropos(1)`
+          and the `-k` option of
+          {manpage}`man(1)`.
         '';
       };
 
@@ -215,16 +209,14 @@ in
       dev.enable = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = mdDoc ''
           Whether to install documentation targeted at developers.
-          <itemizedlist>
-          <listitem><para>This includes man pages targeted at developers if <option>documentation.man.enable</option> is
-                    set (this also includes "devman" outputs).</para></listitem>
-          <listitem><para>This includes info pages targeted at developers if <option>documentation.info.enable</option>
-                    is set (this also includes "devinfo" outputs).</para></listitem>
-          <listitem><para>This includes other pages targeted at developers if <option>documentation.doc.enable</option>
-                    is set (this also includes "devdoc" outputs).</para></listitem>
-          </itemizedlist>
+          * This includes man pages targeted at developers if {option}`documentation.man.enable` is
+            set (this also includes "devman" outputs).
+          * This includes info pages targeted at developers if {option}`documentation.info.enable`
+            is set (this also includes "devinfo" outputs).
+          * This includes other pages targeted at developers if {option}`documentation.doc.enable`
+            is set (this also includes "devdoc" outputs).
         '';
       };
 
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 9d620084308b2..e3d7866cabb51 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -3,7 +3,7 @@
 
 # IMPORTANT!
 # We only add static uids and gids for services where it is not feasible
-# to change uids/gids on service start, in example a service with a lot of
+# to change uids/gids on service start, for example a service with a lot of
 # files. Please also check if the service is applicable for systemd's
 # DynamicUser option and does not need a uid/gid allocation at all.
 # Systemd can also change ownership of service directories using the
@@ -82,14 +82,14 @@ in
       git = 41;
       #fourstore = 42; # dropped in 20.03
       #fourstorehttp = 43; # dropped in 20.03
-      virtuoso = 44;
+      #virtuoso = 44;  dropped module
       #rtkit = 45; # dynamically allocated 2021-09-03
       dovecot2 = 46;
       dovenull2 = 47;
       prayer = 49;
       mpd = 50;
       clamav = 51;
-      fprot = 52;
+      #fprot = 52; # unused
       # bind = 53; #dynamically allocated as of 2021-09-03
       wwwrun = 54;
       #adm = 55; # unused
@@ -236,7 +236,7 @@ in
       gitit = 202;
       riemanntools = 203;
       subsonic = 204;
-      riak = 205;
+      # riak = 205; # unused, remove 2022-07-22
       #shout = 206; # dynamically allocated as of 2021-09-18
       gateone = 207;
       namecoin = 208;
@@ -412,7 +412,7 @@ in
       prayer = 49;
       mpd = 50;
       clamav = 51;
-      fprot = 52;
+      #fprot = 52; # unused
       #bind = 53; # unused
       wwwrun = 54;
       adm = 55;
@@ -553,7 +553,7 @@ in
       gitit = 202;
       riemanntools = 203;
       subsonic = 204;
-      riak = 205;
+      # riak = 205;#unused, removed 2022-06-22
       #shout = 206; #unused
       gateone = 207;
       namecoin = 208;
@@ -667,6 +667,27 @@ in
       # uid. Users and groups with the same name should have equal
       # uids and gids. Also, don't use gids above 399!
 
+      # For exceptional cases where you really need a gid above 399, leave a
+      # comment stating why.
+      #
+      # Also, avoid the following GID ranges:
+      #
+      #  1000 - 29999: user accounts (see ../config/update-users-groups.pl)
+      # 30000 - 31000: nixbld users (the upper limit is arbitrarily chosen)
+      # 61184 - 65519: systemd DynamicUser (see systemd.exec(5))
+      #         65535: the error return sentinel value when uid_t was 16 bits
+      #
+      # 100000 - 6653600: subgid allocated for user namespaces
+      #                   (see ../config/update-users-groups.pl)
+      #       4294967294: unauthenticated user in some NFS implementations
+      #       4294967295: error return sentinel value
+      #
+      # References:
+      # https://www.debian.org/doc/debian-policy/ch-opersys.html#uid-and-gid-classes
+
+      onepassword = 31001; # 1Password requires that its GID be larger than 1000
+      onepassword-cli = 31002; # 1Password requires that its GID be larger than 1000
+
       users = 100;
       nixbld = 30000;
       nogroup = 65534;
diff --git a/nixos/modules/misc/label.nix b/nixos/modules/misc/label.nix
index 02b91555b3c21..b97cbaa26304e 100644
--- a/nixos/modules/misc/label.nix
+++ b/nixos/modules/misc/label.nix
@@ -11,7 +11,7 @@ in
   options.system = {
 
     nixos.label = mkOption {
-      type = types.str;
+      type = types.strMatching "[a-zA-Z0-9:_\\.-]*";
       description = ''
         NixOS version name to be used in the names of generated
         outputs and boot labels.
@@ -19,6 +19,9 @@ in
         If you ever wanted to influence the labels in your GRUB menu,
         this is the option for you.
 
+        It can only contain letters, numbers and the following symbols:
+        <literal>:</literal>, <literal>_</literal>, <literal>.</literal> and <literal>-</literal>.
+
         The default is <option>system.nixos.tags</option> separated by
         "-" + "-" + <envar>NIXOS_LABEL_VERSION</envar> environment
         variable (defaults to the value of
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
index 66a49b0b888f2..50495eebe4c29 100644
--- a/nixos/modules/misc/locate.nix
+++ b/nixos/modules/misc/locate.nix
@@ -27,7 +27,7 @@ in
 
     locate = mkOption {
       type = package;
-      default = pkgs.findutils;
+      default = pkgs.findutils.locate;
       defaultText = literalExpression "pkgs.findutils";
       example = literalExpression "pkgs.mlocate";
       description = ''
@@ -183,7 +183,11 @@ in
 
     pruneNames = mkOption {
       type = listOf str;
-      default = [ ".bzr" ".cache" ".git" ".hg" ".svn" ];
+      default = lib.optionals (!isFindutils) [ ".bzr" ".cache" ".git" ".hg" ".svn" ];
+      defaultText = literalDocBook ''
+        <literal>[ ".bzr" ".cache" ".git" ".hg" ".svn" ]</literal>, if
+        supported by the locate implementation (i.e. mlocate or plocate).
+      '';
       description = ''
         Directory components which should exclude paths containing them from indexing
       '';
@@ -246,7 +250,7 @@ in
     };
 
     warnings = optional (isMorPLocate && cfg.localuser != null)
-      "mlocate does not support the services.locate.localuser option; updatedb will run as root. (Silence with services.locate.localuser = null.)"
+      "mlocate and plocate do not support the services.locate.localuser option. updatedb will run as root. Silence this warning by setting services.locate.localuser = null."
     ++ optional (isFindutils && cfg.pruneNames != [ ])
       "findutils locate does not support pruning by directory component"
     ++ optional (isFindutils && cfg.pruneBindMounts)
diff --git a/nixos/modules/misc/man-db.nix b/nixos/modules/misc/man-db.nix
index 8bd329bc4e0c3..7aeb02d883ac8 100644
--- a/nixos/modules/misc/man-db.nix
+++ b/nixos/modules/misc/man-db.nix
@@ -23,11 +23,11 @@ in
             ++ lib.optionals config.documentation.dev.enable [ "devman" ];
           ignoreCollisions = true;
         };
-        defaultText = lib.literalDocBook "all man pages in <option>config.environment.systemPackages</option>";
-        description = ''
-          The manual pages to generate caches for if <option>documentation.man.generateCaches</option>
+        defaultText = lib.literalMD "all man pages in {option}`config.environment.systemPackages`";
+        description = lib.mdDoc ''
+          The manual pages to generate caches for if {option}`documentation.man.generateCaches`
           is enabled. Must be a path to a directory with man pages under
-          <literal>/share/man</literal>; see the source for an example.
+          `/share/man`; see the source for an example.
           Advanced users can make this a content-addressed derivation to save a few rebuilds.
         '';
       };
diff --git a/nixos/modules/misc/mandoc.nix b/nixos/modules/misc/mandoc.nix
index 3da60f2f8e65f..838f20876563a 100644
--- a/nixos/modules/misc/mandoc.nix
+++ b/nixos/modules/misc/mandoc.nix
@@ -53,7 +53,9 @@ in {
       # see: https://inbox.vuxu.org/mandoc-tech/20210906171231.GF83680@athene.usta.de/T/#e85f773c1781e3fef85562b2794f9cad7b2909a3c
       extraSetup = lib.mkIf config.documentation.man.generateCaches ''
         ${makewhatis} -T utf8 ${
-          lib.concatMapStringsSep " " (path: "\"$out/${path}\"") cfg.manPath
+          lib.concatMapStringsSep " " (path:
+            "$out/" + lib.escapeShellArg path
+          ) cfg.manPath
         }
       '';
     };
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
index 69967c8a7601e..ad017aff816c7 100644
--- a/nixos/modules/misc/nixpkgs.nix
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -55,9 +55,46 @@ let
     check = builtins.isAttrs;
   };
 
-  defaultPkgs = import ../../.. {
-    inherit (cfg) config overlays localSystem crossSystem;
-  };
+  hasBuildPlatform = opt.buildPlatform.highestPrio < (mkOptionDefault {}).priority;
+  hasHostPlatform = opt.hostPlatform.isDefined;
+  hasPlatform = hasHostPlatform || hasBuildPlatform;
+
+  # Context for messages
+  hostPlatformLine = optionalString hasHostPlatform "${showOptionWithDefLocs opt.hostPlatform}";
+  buildPlatformLine = optionalString hasBuildPlatform "${showOptionWithDefLocs opt.buildPlatform}";
+  platformLines = optionalString hasPlatform ''
+    Your system configuration configures nixpkgs with platform parameters:
+    ${hostPlatformLine
+    }${buildPlatformLine
+    }'';
+
+  legacyOptionsDefined =
+    optional (opt.localSystem.highestPrio < (mkDefault {}).priority) opt.system
+    ++ optional (opt.localSystem.highestPrio < (mkOptionDefault {}).priority) opt.localSystem
+    ++ optional (opt.crossSystem.highestPrio < (mkOptionDefault {}).priority) opt.crossSystem
+    ;
+
+  defaultPkgs =
+    if opt.hostPlatform.isDefined
+    then
+      let isCross = cfg.buildPlatform != cfg.hostPlatform;
+          systemArgs =
+            if isCross
+            then {
+              localSystem = cfg.buildPlatform;
+              crossSystem = cfg.hostPlatform;
+            }
+            else {
+              localSystem = cfg.hostPlatform;
+            };
+      in
+      import ../../.. ({
+        inherit (cfg) config overlays;
+      } // systemArgs)
+    else
+      import ../../.. {
+        inherit (cfg) config overlays localSystem crossSystem;
+      };
 
   finalPkgs = if opt.pkgs.isDefined then cfg.pkgs.appendOverlays cfg.overlays else defaultPkgs;
 
@@ -67,6 +104,7 @@ in
   imports = [
     ./assertions.nix
     ./meta.nix
+    (mkRemovedOptionModule [ "nixpkgs" "initialSystem" ] "The NixOS options `nesting.clone` and `nesting.children` have been deleted, and replaced with named specialisation. Therefore `nixpgks.initialSystem` has no effect anymore.")
   ];
 
   options.nixpkgs = {
@@ -156,6 +194,46 @@ in
       '';
     };
 
+    hostPlatform = mkOption {
+      type = types.either types.str types.attrs; # TODO utilize lib.systems.parsedPlatform
+      example = { system = "aarch64-linux"; config = "aarch64-unknown-linux-gnu"; };
+      # Make sure that the final value has all fields for sake of other modules
+      # referring to this. TODO make `lib.systems` itself use the module system.
+      apply = lib.systems.elaborate;
+      defaultText = literalExpression
+        ''(import "''${nixos}/../lib").lib.systems.examples.aarch64-multiplatform'';
+      description = ''
+        Specifies the platform where the NixOS configuration will run.
+
+        To cross-compile, set also <code>nixpkgs.buildPlatform</code>.
+
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
+    buildPlatform = mkOption {
+      type = types.either types.str types.attrs; # TODO utilize lib.systems.parsedPlatform
+      default = cfg.hostPlatform;
+      example = { system = "x86_64-linux"; config = "x86_64-unknown-linux-gnu"; };
+      # Make sure that the final value has all fields for sake of other modules
+      # referring to this.
+      apply = lib.systems.elaborate;
+      defaultText = literalExpression
+        ''config.nixpkgs.hostPlatform'';
+      description = ''
+        Specifies the platform on which NixOS should be built.
+        By default, NixOS is built on the system where it runs, but you can
+        change where it's built. Setting this option will cause NixOS to be
+        cross-compiled.
+
+        For instance, if you're doing distributed multi-platform deployment,
+        or if you're building machines, you can set this to match your
+        development system and/or build farm.
+
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
     localSystem = mkOption {
       type = types.attrs; # TODO utilize lib.systems.parsedPlatform
       default = { inherit (cfg) system; };
@@ -175,10 +253,13 @@ in
         deployment, or when building virtual machines. See its
         description in the Nixpkgs manual for more details.
 
-        Ignored when <code>nixpkgs.pkgs</code> is set.
+        Ignored when <code>nixpkgs.pkgs</code> or <code>hostPlatform</code> is set.
       '';
     };
 
+    # TODO deprecate. "crossSystem" is a nonsense identifier, because "cross"
+    #      is a relation between at least 2 systems in the context of a
+    #      specific build step, not a single system.
     crossSystem = mkOption {
       type = types.nullOr types.attrs; # TODO utilize lib.systems.parsedPlatform
       default = null;
@@ -192,7 +273,7 @@ in
         should be set as null, the default. See its description in the
         Nixpkgs manual for more details.
 
-        Ignored when <code>nixpkgs.pkgs</code> is set.
+        Ignored when <code>nixpkgs.pkgs</code> or <code>hostPlatform</code> is set.
       '';
     };
 
@@ -215,16 +296,7 @@ in
         </programlisting>
         See <code>nixpkgs.localSystem</code> for more information.
 
-        Ignored when <code>nixpkgs.localSystem</code> is set.
-        Ignored when <code>nixpkgs.pkgs</code> is set.
-      '';
-    };
-
-    initialSystem = mkOption {
-      type = types.str;
-      internal = true;
-      description = ''
-        Preserved value of <literal>system</literal> passed to <literal>eval-config.nix</literal>.
+        Ignored when <code>nixpkgs.pkgs</code>, <code>nixpkgs.localSystem</code> or <code>nixpkgs.hostPlatform</code> is set.
       '';
     };
   };
@@ -247,10 +319,23 @@ in
             else "nixpkgs.localSystem";
           pkgsSystem = finalPkgs.stdenv.targetPlatform.system;
         in {
-          assertion = nixosExpectedSystem == pkgsSystem;
+          assertion = !hasPlatform -> nixosExpectedSystem == pkgsSystem;
           message = "The NixOS nixpkgs.pkgs option was set to a Nixpkgs invocation that compiles to target system ${pkgsSystem} but NixOS was configured for system ${nixosExpectedSystem} via NixOS option ${nixosOption}. The NixOS system settings must match the Nixpkgs target system.";
         }
       )
+      {
+        assertion = hasPlatform -> legacyOptionsDefined == [];
+        message = ''
+          Your system configures nixpkgs with the platform parameter${optionalString hasBuildPlatform "s"}:
+          ${hostPlatformLine
+          }${buildPlatformLine
+          }
+          However, it also defines the legacy options:
+          ${concatMapStrings showOptionWithDefLocs legacyOptionsDefined}
+          For a future proof system configuration, we recommend to remove
+          the legacy definitions.
+        '';
+      }
     ];
   };
 
diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix
index ec5fab9fb4a5e..9e8851707f8fc 100644
--- a/nixos/modules/misc/nixpkgs/test.nix
+++ b/nixos/modules/misc/nixpkgs/test.nix
@@ -1,8 +1,63 @@
 { evalMinimalConfig, pkgs, lib, stdenv }:
+let
+  eval = mod: evalMinimalConfig {
+    imports = [ ../nixpkgs.nix mod ];
+  };
+  withHost = eval {
+    nixpkgs.hostPlatform = "aarch64-linux";
+  };
+  withHostAndBuild = eval {
+    nixpkgs.hostPlatform = "aarch64-linux";
+    nixpkgs.buildPlatform = "aarch64-darwin";
+  };
+  ambiguous = {
+    _file = "ambiguous.nix";
+    nixpkgs.hostPlatform = "aarch64-linux";
+    nixpkgs.buildPlatform = "aarch64-darwin";
+    nixpkgs.system = "x86_64-linux";
+    nixpkgs.localSystem.system = "x86_64-darwin";
+    nixpkgs.crossSystem.system = "i686-linux";
+    imports = [
+      { _file = "repeat.nix";
+        nixpkgs.hostPlatform = "aarch64-linux";
+      }
+    ];
+  };
+  getErrors = module:
+    let
+      uncheckedEval = lib.evalModules { modules = [ ../nixpkgs.nix module ]; };
+    in map (ass: ass.message) (lib.filter (ass: !ass.assertion) uncheckedEval.config.assertions);
+in
 lib.recurseIntoAttrs {
   invokeNixpkgsSimple =
-    (evalMinimalConfig ({ config, modulesPath, ... }: {
-      imports = [ (modulesPath + "/misc/nixpkgs.nix") ];
+    (eval {
       nixpkgs.system = stdenv.hostPlatform.system;
-    }))._module.args.pkgs.hello;
+    })._module.args.pkgs.hello;
+  assertions =
+    assert withHost._module.args.pkgs.stdenv.hostPlatform.system == "aarch64-linux";
+    assert withHost._module.args.pkgs.stdenv.buildPlatform.system == "aarch64-linux";
+    assert withHostAndBuild._module.args.pkgs.stdenv.hostPlatform.system == "aarch64-linux";
+    assert withHostAndBuild._module.args.pkgs.stdenv.buildPlatform.system == "aarch64-darwin";
+    assert builtins.trace (lib.head (getErrors ambiguous))
+      getErrors ambiguous ==
+        [''
+          Your system configures nixpkgs with the platform parameters:
+          nixpkgs.hostPlatform, with values defined in:
+            - repeat.nix
+            - ambiguous.nix
+          nixpkgs.buildPlatform, with values defined in:
+            - ambiguous.nix
+
+          However, it also defines the legacy options:
+          nixpkgs.system, with values defined in:
+            - ambiguous.nix
+          nixpkgs.localSystem, with values defined in:
+            - ambiguous.nix
+          nixpkgs.crossSystem, with values defined in:
+            - ambiguous.nix
+
+          For a future proof system configuration, we recommend to remove
+          the legacy definitions.
+        ''];
+    pkgs.emptyFile;
 }
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 6c526f6d4f2db..da458a5748401 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -1,12 +1,41 @@
 { config, lib, options, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.system.nixos;
   opt = options.system.nixos;
-in
 
+  inherit (lib)
+    concatStringsSep mapAttrsToList toLower
+    literalExpression mkRenamedOptionModule mkDefault mkOption trivial types;
+
+  needsEscaping = s: null != builtins.match "[a-zA-Z0-9]+" s;
+  escapeIfNeccessary = s: if needsEscaping s then s else ''"${lib.escape [ "\$" "\"" "\\" "\`" ] s}"'';
+  attrsToText = attrs:
+    concatStringsSep "\n" (
+      mapAttrsToList (n: v: ''${n}=${escapeIfNeccessary (toString v)}'') attrs
+    ) + "\n";
+
+  osReleaseContents = {
+    NAME = "NixOS";
+    ID = "nixos";
+    VERSION = "${cfg.release} (${cfg.codeName})";
+    VERSION_CODENAME = toLower cfg.codeName;
+    VERSION_ID = cfg.release;
+    BUILD_ID = cfg.version;
+    PRETTY_NAME = "NixOS ${cfg.release} (${cfg.codeName})";
+    LOGO = "nix-snowflake";
+    HOME_URL = "https://nixos.org/";
+    DOCUMENTATION_URL = "https://nixos.org/learn.html";
+    SUPPORT_URL = "https://nixos.org/community.html";
+    BUG_REPORT_URL = "https://github.com/NixOS/nixpkgs/issues";
+  };
+
+  initrdReleaseContents = osReleaseContents // {
+    PRETTY_NAME = "${osReleaseContents.PRETTY_NAME} (Initrd)";
+  };
+  initrdRelease = pkgs.writeText "initrd-release" (attrsToText initrdReleaseContents);
+
+in
 {
   imports = [
     (mkRenamedOptionModule [ "system" "nixosVersion" ] [ "system" "nixos" "version" ])
@@ -101,22 +130,31 @@ in
     # Generate /etc/os-release.  See
     # https://www.freedesktop.org/software/systemd/man/os-release.html for the
     # format.
-    environment.etc.os-release.text =
-      ''
-        NAME=NixOS
-        ID=nixos
-        VERSION="${cfg.release} (${cfg.codeName})"
-        VERSION_CODENAME=${toLower cfg.codeName}
-        VERSION_ID="${cfg.release}"
-        BUILD_ID="${cfg.version}"
-        PRETTY_NAME="NixOS ${cfg.release} (${cfg.codeName})"
-        LOGO="nix-snowflake"
-        HOME_URL="https://nixos.org/"
-        DOCUMENTATION_URL="https://nixos.org/learn.html"
-        SUPPORT_URL="https://nixos.org/community.html"
-        BUG_REPORT_URL="https://github.com/NixOS/nixpkgs/issues"
-      '';
+    environment.etc = {
+      "lsb-release".text = attrsToText {
+        LSB_VERSION = "${cfg.release} (${cfg.codeName})";
+        DISTRIB_ID = "nixos";
+        DISTRIB_RELEASE = cfg.release;
+        DISTRIB_CODENAME = toLower cfg.codeName;
+        DISTRIB_DESCRIPTION = "NixOS ${cfg.release} (${cfg.codeName})";
+      };
+
+      "os-release".text = attrsToText osReleaseContents;
+    };
+
+    boot.initrd.systemd.contents = {
+      "/etc/os-release".source = initrdRelease;
+      "/etc/initrd-release".source = initrdRelease;
+    };
 
+    # We have to use `warnings` because when warning in the default of the option
+    # the warning would also be shown when building the manual since the manual
+    # has to evaluate the default.
+    #
+    # TODO Remove this and drop the default of the option so people are forced to set it.
+    # Doing this also means fixing the comment in nixos/modules/testing/test-instrumentation.nix
+    warnings = lib.optional (options.system.stateVersion.highestPrio == (lib.mkOptionDefault { }).priority)
+      "system.stateVersion is not set, defaulting to ${config.system.stateVersion}. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.";
   };
 
   # uses version info nixpkgs, which requires a full nixpkgs path
diff --git a/nixos/modules/misc/wordlist.nix b/nixos/modules/misc/wordlist.nix
new file mode 100644
index 0000000000000..988b522d74314
--- /dev/null
+++ b/nixos/modules/misc/wordlist.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  concatAndSort = name: files: pkgs.runCommand name {} ''
+    awk 1 ${lib.escapeShellArgs files} | sed '{ /^\s*$/d; s/^\s\+//; s/\s\+$// }' | sort | uniq > $out
+  '';
+in
+{
+  options = {
+    environment.wordlist = {
+      enable = mkEnableOption "environment variables for lists of words";
+
+      lists = mkOption {
+        type = types.attrsOf (types.nonEmptyListOf types.path);
+
+        default = {
+          WORDLIST = [ "${pkgs.scowl}/share/dict/words.txt" ];
+        };
+
+        defaultText = literalExpression ''
+          {
+            WORDLIST = [ "''${pkgs.scowl}/share/dict/words.txt" ];
+          }
+        '';
+
+        description = ''
+          A set with the key names being the environment variable you'd like to
+          set and the values being a list of paths to text documents containing
+          lists of words. The various files will be merged, sorted, duplicates
+          removed, and extraneous spacing removed.
+
+          If you have a handful of words that you want to add to an already
+          existing wordlist, you may find `builtins.toFile` useful for this
+          task.
+        '';
+
+        example = literalExpression ''
+          {
+            WORDLIST = [ "''${pkgs.scowl}/share/dict/words.txt" ];
+            AUGMENTED_WORDLIST = [
+              "''${pkgs.scowl}/share/dict/words.txt"
+              "''${pkgs.scowl}/share/dict/words.variants.txt"
+              (builtins.toFile "extra-words" '''
+                desynchonization
+                oobleck''')
+            ];
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.environment.wordlist.enable {
+    environment.variables =
+      lib.mapAttrs
+        (name: value: "${concatAndSort "wordlist-${name}" value}")
+        config.environment.wordlist.lists;
+  };
+}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index e782e79661ef1..3010a213705b2 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -10,6 +10,7 @@
   ./config/xdg/mime.nix
   ./config/xdg/portal.nix
   ./config/xdg/portals/wlr.nix
+  ./config/xdg/portals/lxqt.nix
   ./config/appstream.nix
   ./config/console.nix
   ./config/xdg/sounds.nix
@@ -57,6 +58,7 @@
   ./hardware/sensor/hddtemp.nix
   ./hardware/sensor/iio.nix
   ./hardware/keyboard/teck.nix
+  ./hardware/keyboard/uhk.nix
   ./hardware/keyboard/zsa.nix
   ./hardware/ksm.nix
   ./hardware/ledger.nix
@@ -65,6 +67,7 @@
   ./hardware/network/ath-user-regd.nix
   ./hardware/network/b43.nix
   ./hardware/network/intel-2200bg.nix
+  ./hardware/new-lg4ff.nix
   ./hardware/nitrokey.nix
   ./hardware/opengl.nix
   ./hardware/openrazer.nix
@@ -72,6 +75,7 @@
   ./hardware/printers.nix
   ./hardware/raid/hpsa.nix
   ./hardware/rtl-sdr.nix
+  ./hardware/saleae-logic.nix
   ./hardware/steam-hardware.nix
   ./hardware/system-76.nix
   ./hardware/tuxedo-keyboard.nix
@@ -91,6 +95,7 @@
   ./hardware/video/switcheroo-control.nix
   ./hardware/video/uvcvideo/default.nix
   ./hardware/video/webcam/facetimehd.nix
+  ./hardware/xone.nix
   ./hardware/xpadneo.nix
   ./i18n/input-method/default.nix
   ./i18n/input-method/fcitx.nix
@@ -115,7 +120,10 @@
   ./misc/nixpkgs.nix
   ./misc/passthru.nix
   ./misc/version.nix
+  ./misc/wordlist.nix
   ./misc/nixops-autoluks.nix
+  ./programs/_1password.nix
+  ./programs/_1password-gui.nix
   ./programs/adb.nix
   ./programs/appgate-sdp.nix
   ./programs/atop.nix
@@ -132,8 +140,8 @@
   ./programs/captive-browser.nix
   ./programs/ccache.nix
   ./programs/cdemu.nix
+  ./programs/cfs-zen-tweaks.nix
   ./programs/chromium.nix
-  ./programs/clickshare.nix
   ./programs/cnping.nix
   ./programs/command-not-found/command-not-found.nix
   ./programs/criu.nix
@@ -161,11 +169,15 @@
   ./programs/gpaste.nix
   ./programs/gnupg.nix
   ./programs/gphoto2.nix
+  ./programs/haguichi.nix
   ./programs/hamster.nix
   ./programs/htop.nix
   ./programs/iftop.nix
   ./programs/iotop.nix
   ./programs/java.nix
+  ./programs/k40-whisperer.nix
+  ./programs/kclock.nix
+  ./programs/k3b.nix
   ./programs/kdeconnect.nix
   ./programs/kbdlight.nix
   ./programs/less.nix
@@ -176,16 +188,20 @@
   ./programs/msmtp.nix
   ./programs/mtr.nix
   ./programs/nano.nix
+  ./programs/nbd.nix
+  ./programs/nix-ld.nix
   ./programs/neovim.nix
+  ./programs/nethoscope.nix
   ./programs/nm-applet.nix
+  ./programs/nncp.nix
   ./programs/npm.nix
   ./programs/noisetorch.nix
   ./programs/oblogout.nix
+  ./programs/openvpn3.nix
   ./programs/pantheon-tweaks.nix
   ./programs/partition-manager.nix
   ./programs/plotinus.nix
   ./programs/proxychains.nix
-  ./programs/phosh.nix
   ./programs/qt5ct.nix
   ./programs/screen.nix
   ./programs/sedutil.nix
@@ -195,14 +211,15 @@
   ./programs/spacefm.nix
   ./programs/singularity.nix
   ./programs/ssh.nix
-  ./programs/ssmtp.nix
   ./programs/sysdig.nix
   ./programs/systemtap.nix
   ./programs/starship.nix
   ./programs/steam.nix
+  ./programs/streamdeck-ui.nix
   ./programs/sway.nix
   ./programs/system-config-printer.nix
   ./programs/thefuck.nix
+  ./programs/thunar.nix
   ./programs/tmux.nix
   ./programs/traceroute.nix
   ./programs/tsm-client.nix
@@ -215,6 +232,7 @@
   ./programs/weylus.nix
   ./programs/wireshark.nix
   ./programs/wshowkeys.nix
+  ./programs/xfconf.nix
   ./programs/xfs_quota.nix
   ./programs/xonsh.nix
   ./programs/xss-lock.nix
@@ -252,6 +270,7 @@
   ./security/tpm2.nix
   ./services/admin/meshcentral.nix
   ./services/admin/oxidized.nix
+  ./services/admin/pgadmin.nix
   ./services/admin/salt/master.nix
   ./services/admin/salt/minion.nix
   ./services/amqp/activemq/default.nix
@@ -296,6 +315,7 @@
   ./services/backup/znapzend.nix
   ./services/blockchain/ethereum/geth.nix
   ./services/backup/zrepl.nix
+  ./services/cluster/corosync/default.nix
   ./services/cluster/hadoop/default.nix
   ./services/cluster/k3s/default.nix
   ./services/cluster/kubernetes/addons/dns.nix
@@ -308,6 +328,7 @@
   ./services/cluster/kubernetes/pki.nix
   ./services/cluster/kubernetes/proxy.nix
   ./services/cluster/kubernetes/scheduler.nix
+  ./services/cluster/pacemaker/default.nix
   ./services/cluster/spark/default.nix
   ./services/computing/boinc/client.nix
   ./services/computing/foldingathome/client.nix
@@ -332,6 +353,8 @@
   ./services/databases/clickhouse.nix
   ./services/databases/cockroachdb.nix
   ./services/databases/couchdb.nix
+  ./services/databases/dragonflydb.nix
+  ./services/databases/dgraph.nix
   ./services/databases/firebird.nix
   ./services/databases/foundationdb.nix
   ./services/databases/hbase.nix
@@ -347,9 +370,7 @@
   ./services/databases/pgmanage.nix
   ./services/databases/postgresql.nix
   ./services/databases/redis.nix
-  ./services/databases/riak.nix
   ./services/databases/victoriametrics.nix
-  ./services/databases/virtuoso.nix
   ./services/desktops/accountsservice.nix
   ./services/desktops/bamf.nix
   ./services/desktops/blueman.nix
@@ -394,9 +415,11 @@
   ./services/development/jupyterhub/default.nix
   ./services/development/rstudio-server/default.nix
   ./services/development/lorri.nix
+  ./services/development/zammad.nix
   ./services/display-managers/greetd.nix
   ./services/editors/emacs.nix
   ./services/editors/infinoted.nix
+  ./services/editors/haste.nix
   ./services/finance/odoo.nix
   ./services/games/asf.nix
   ./services/games/crossfire-server.nix
@@ -411,6 +434,7 @@
   ./services/games/terraria.nix
   ./services/hardware/acpid.nix
   ./services/hardware/actkbd.nix
+  ./services/hardware/argonone.nix
   ./services/hardware/auto-cpufreq.nix
   ./services/hardware/bluetooth.nix
   ./services/hardware/bolt.nix
@@ -446,10 +470,13 @@
   ./services/hardware/udisks2.nix
   ./services/hardware/upower.nix
   ./services/hardware/usbmuxd.nix
+  ./services/hardware/usbrelayd.nix
   ./services/hardware/thermald.nix
   ./services/hardware/undervolt.nix
   ./services/hardware/vdr.nix
   ./services/hardware/xow.nix
+  ./services/home-automation/home-assistant.nix
+  ./services/home-automation/zigbee2mqtt.nix
   ./services/logging/SystemdJournal2Gelf.nix
   ./services/logging/awstats.nix
   ./services/logging/filebeat.nix
@@ -488,14 +515,23 @@
   ./services/mail/postfixadmin.nix
   ./services/mail/postsrsd.nix
   ./services/mail/postgrey.nix
+  ./services/mail/public-inbox.nix
   ./services/mail/spamassassin.nix
   ./services/mail/rspamd.nix
   ./services/mail/rss2email.nix
   ./services/mail/roundcube.nix
+  ./services/mail/schleuder.nix
   ./services/mail/sympa.nix
   ./services/mail/nullmailer.nix
+  ./services/matrix/appservice-discord.nix
+  ./services/matrix/appservice-irc.nix
+  ./services/matrix/conduit.nix
+  ./services/matrix/dendrite.nix
+  ./services/matrix/mautrix-facebook.nix
+  ./services/matrix/mautrix-telegram.nix
   ./services/matrix/mjolnir.nix
   ./services/matrix/pantalaimon.nix
+  ./services/matrix/synapse.nix
   ./services/misc/ananicy.nix
   ./services/misc/airsonic.nix
   ./services/misc/ankisyncd.nix
@@ -514,7 +550,6 @@
   ./services/misc/cpuminer-cryptonight.nix
   ./services/misc/cgminer.nix
   ./services/misc/confd.nix
-  ./services/misc/dendrite.nix
   ./services/misc/devmon.nix
   ./services/misc/dictd.nix
   ./services/misc/duckling.nix
@@ -545,8 +580,8 @@
   ./services/misc/headphones.nix
   ./services/misc/heisenbridge.nix
   ./services/misc/greenclip.nix
-  ./services/misc/home-assistant.nix
   ./services/misc/ihaskell.nix
+  ./services/misc/input-remapper.nix
   ./services/misc/irkerd.nix
   ./services/misc/jackett.nix
   ./services/misc/jellyfin.nix
@@ -557,12 +592,6 @@
   ./services/misc/libreddit.nix
   ./services/misc/lifecycled.nix
   ./services/misc/mame.nix
-  ./services/misc/matrix-appservice-discord.nix
-  ./services/misc/matrix-appservice-irc.nix
-  ./services/misc/matrix-conduit.nix
-  ./services/misc/matrix-synapse.nix
-  ./services/misc/mautrix-facebook.nix
-  ./services/misc/mautrix-telegram.nix
   ./services/misc/mbpfan.nix
   ./services/misc/mediatomb.nix
   ./services/misc/metabase.nix
@@ -582,11 +611,13 @@
   ./services/misc/osrm.nix
   ./services/misc/owncast.nix
   ./services/misc/packagekit.nix
-  ./services/misc/paperless-ng.nix
+  ./services/misc/paperless.nix
   ./services/misc/parsoid.nix
+  ./services/misc/persistent-evdev.nix
   ./services/misc/plex.nix
   ./services/misc/plikd.nix
   ./services/misc/podgrab.nix
+  ./services/misc/polaris.nix
   ./services/misc/prowlarr.nix
   ./services/misc/tautulli.nix
   ./services/misc/pinnwand.nix
@@ -621,7 +652,6 @@
   ./services/misc/weechat.nix
   ./services/misc/xmr-stak.nix
   ./services/misc/xmrig.nix
-  ./services/misc/zigbee2mqtt.nix
   ./services/misc/zoneminder.nix
   ./services/misc/zookeeper.nix
   ./services/monitoring/alerta.nix
@@ -636,6 +666,7 @@
   ./services/monitoring/do-agent.nix
   ./services/monitoring/fusion-inventory.nix
   ./services/monitoring/grafana.nix
+  ./services/monitoring/grafana-agent.nix
   ./services/monitoring/grafana-image-renderer.nix
   ./services/monitoring/grafana-reporter.nix
   ./services/monitoring/graphite.nix
@@ -647,6 +678,7 @@
   ./services/monitoring/longview.nix
   ./services/monitoring/mackerel-agent.nix
   ./services/monitoring/metricbeat.nix
+  ./services/monitoring/mimir.nix
   ./services/monitoring/monit.nix
   ./services/monitoring/munin.nix
   ./services/monitoring/nagios.nix
@@ -715,16 +747,20 @@
   ./services/networking/bitcoind.nix
   ./services/networking/autossh.nix
   ./services/networking/bird.nix
+  ./services/networking/bird-lg.nix
   ./services/networking/bitlbee.nix
   ./services/networking/blockbook-frontend.nix
+  ./services/networking/blocky.nix
   ./services/networking/charybdis.nix
   ./services/networking/cjdns.nix
+  ./services/networking/cloudflare-dyndns.nix
   ./services/networking/cntlm.nix
   ./services/networking/connman.nix
   ./services/networking/consul.nix
   ./services/networking/coredns.nix
   ./services/networking/corerad.nix
   ./services/networking/coturn.nix
+  ./services/networking/create_ap.nix
   ./services/networking/croc.nix
   ./services/networking/dante.nix
   ./services/networking/ddclient.nix
@@ -739,10 +775,12 @@
   ./services/networking/ncdns.nix
   ./services/networking/nomad.nix
   ./services/networking/ejabberd.nix
+  ./services/networking/envoy.nix
   ./services/networking/epmd.nix
   ./services/networking/ergo.nix
   ./services/networking/ergochat.nix
   ./services/networking/eternal-terminal.nix
+  ./services/networking/expressvpn.nix
   ./services/networking/fakeroute.nix
   ./services/networking/ferm.nix
   ./services/networking/fireqos.nix
@@ -766,6 +804,7 @@
   ./services/networking/headscale.nix
   ./services/networking/hostapd.nix
   ./services/networking/htpdate.nix
+  ./services/networking/https-dns-proxy.nix
   ./services/networking/hylafax/default.nix
   ./services/networking/i2pd.nix
   ./services/networking/i2p.nix
@@ -791,6 +830,7 @@
   ./services/networking/libreswan.nix
   ./services/networking/lldpd.nix
   ./services/networking/logmein-hamachi.nix
+  ./services/networking/lokinet.nix
   ./services/networking/lxd-image-server.nix
   ./services/networking/magic-wormhole-mailbox-server.nix
   ./services/networking/matterbridge.nix
@@ -800,6 +840,7 @@
   ./services/networking/mosquitto.nix
   ./services/networking/monero.nix
   ./services/networking/morty.nix
+  ./services/networking/mozillavpn.nix
   ./services/networking/miredo.nix
   ./services/networking/mstpd.nix
   ./services/networking/mtprotoproxy.nix
@@ -812,6 +853,7 @@
   ./services/networking/nar-serve.nix
   ./services/networking/nat.nix
   ./services/networking/nats.nix
+  ./services/networking/nbd.nix
   ./services/networking/ndppd.nix
   ./services/networking/nebula.nix
   ./services/networking/networkmanager.nix
@@ -834,7 +876,7 @@
   ./services/networking/ofono.nix
   ./services/networking/oidentd.nix
   ./services/networking/onedrive.nix
-  ./services/networking/openfire.nix
+  ./services/networking/openconnect.nix
   ./services/networking/openvpn.nix
   ./services/networking/ostinato.nix
   ./services/networking/owamp.nix
@@ -852,12 +894,14 @@
   ./services/networking/quassel.nix
   ./services/networking/quorum.nix
   ./services/networking/quicktun.nix
+  ./services/networking/r53-ddns.nix
   ./services/networking/radicale.nix
   ./services/networking/radvd.nix
   ./services/networking/rdnssd.nix
   ./services/networking/redsocks.nix
   ./services/networking/resilio.nix
   ./services/networking/robustirc-bridge.nix
+  ./services/networking/routedns.nix
   ./services/networking/rpcbind.nix
   ./services/networking/rxe.nix
   ./services/networking/sabnzbd.nix
@@ -871,6 +915,7 @@
   ./services/networking/shorewall6.nix
   ./services/networking/shout.nix
   ./services/networking/sniproxy.nix
+  ./services/networking/snowflake-proxy.nix
   ./services/networking/smartdns.nix
   ./services/networking/smokeping.nix
   ./services/networking/softether.nix
@@ -895,6 +940,7 @@
   ./services/networking/tcpcrypt.nix
   ./services/networking/teamspeak3.nix
   ./services/networking/tedicross.nix
+  ./services/networking/tetrd.nix
   ./services/networking/teleport.nix
   ./services/networking/thelounge.nix
   ./services/networking/tinc.nix
@@ -910,6 +956,7 @@
   ./services/networking/unifi.nix
   ./services/video/unifi-video.nix
   ./services/video/rtsp-simple-server.nix
+  ./services/networking/uptermd.nix
   ./services/networking/v2ray.nix
   ./services/networking/vsftpd.nix
   ./services/networking/wasabibackend.nix
@@ -944,22 +991,25 @@
   ./services/security/clamav.nix
   ./services/security/fail2ban.nix
   ./services/security/fprintd.nix
-  ./services/security/fprot.nix
   ./services/security/haka.nix
   ./services/security/haveged.nix
   ./services/security/hockeypuck.nix
   ./services/security/hologram-server.nix
   ./services/security/hologram-agent.nix
+  ./services/security/kanidm.nix
+  ./services/security/infnoise.nix
   ./services/security/munge.nix
   ./services/security/nginx-sso.nix
   ./services/security/oauth2_proxy.nix
   ./services/security/oauth2_proxy_nginx.nix
   ./services/security/opensnitch.nix
+  ./services/security/pass-secret-service.nix
   ./services/security/privacyidea.nix
   ./services/security/physlock.nix
   ./services/security/shibboleth-sp.nix
   ./services/security/sks.nix
   ./services/security/sshguard.nix
+  ./services/security/sslmate-agent.nix
   ./services/security/step-ca.nix
   ./services/security/tor.nix
   ./services/security/torify.nix
@@ -977,6 +1027,7 @@
   ./services/system/nscd.nix
   ./services/system/saslauthd.nix
   ./services/system/self-deploy.nix
+  ./services/system/systembus-notify.nix
   ./services/system/uptimed.nix
   ./services/torrent/deluge.nix
   ./services/torrent/flexget.nix
@@ -985,6 +1036,7 @@
   ./services/torrent/peerflix.nix
   ./services/torrent/rtorrent.nix
   ./services/torrent/transmission.nix
+  ./services/tracing/tempo.nix
   ./services/ttys/getty.nix
   ./services/ttys/gpm.nix
   ./services/ttys/kmscon.nix
@@ -1000,7 +1052,6 @@
   ./services/web-apps/code-server.nix
   ./services/web-apps/baget.nix
   ./services/web-apps/convos.nix
-  ./services/web-apps/cryptpad.nix
   ./services/web-apps/dex.nix
   ./services/web-apps/discourse.nix
   ./services/web-apps/documize.nix
@@ -1012,6 +1063,7 @@
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
   ./services/web-apps/grocy.nix
+  ./services/web-apps/healthchecks.nix
   ./services/web-apps/hedgedoc.nix
   ./services/web-apps/hledger-web.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
@@ -1030,9 +1082,13 @@
   ./services/web-apps/mediawiki.nix
   ./services/web-apps/miniflux.nix
   ./services/web-apps/moodle.nix
+  ./services/web-apps/netbox.nix
   ./services/web-apps/nextcloud.nix
   ./services/web-apps/nexus.nix
+  ./services/web-apps/nifi.nix
   ./services/web-apps/node-red.nix
+  ./services/web-apps/phylactery.nix
+  ./services/web-apps/onlyoffice.nix
   ./services/web-apps/pict-rs.nix
   ./services/web-apps/peertube.nix
   ./services/web-apps/plantuml-server.nix
@@ -1049,6 +1105,7 @@
   ./services/web-apps/trilium.nix
   ./services/web-apps/selfoss.nix
   ./services/web-apps/shiori.nix
+  ./services/web-apps/snipe-it.nix
   ./services/web-apps/vikunja.nix
   ./services/web-apps/virtlyst.nix
   ./services/web-apps/wiki-js.nix
@@ -1056,6 +1113,7 @@
   ./services/web-apps/wordpress.nix
   ./services/web-apps/youtrack.nix
   ./services/web-apps/zabbix.nix
+  ./services/web-servers/agate.nix
   ./services/web-servers/apache-httpd/default.nix
   ./services/web-servers/caddy/default.nix
   ./services/web-servers/darkhttpd.nix
@@ -1154,7 +1212,15 @@
   ./system/boot/stage-1.nix
   ./system/boot/stage-2.nix
   ./system/boot/systemd.nix
-  ./system/boot/systemd-nspawn.nix
+  ./system/boot/systemd/coredump.nix
+  ./system/boot/systemd/initrd-secrets.nix
+  ./system/boot/systemd/initrd.nix
+  ./system/boot/systemd/journald.nix
+  ./system/boot/systemd/logind.nix
+  ./system/boot/systemd/nspawn.nix
+  ./system/boot/systemd/shutdown.nix
+  ./system/boot/systemd/tmpfiles.nix
+  ./system/boot/systemd/user.nix
   ./system/boot/timesyncd.nix
   ./system/boot/tmp.nix
   ./system/etc/etc-activation.nix
@@ -1191,6 +1257,7 @@
   ./tasks/powertop.nix
   ./testing/service-runner.nix
   ./virtualisation/anbox.nix
+  ./virtualisation/appvm.nix
   ./virtualisation/build-vm.nix
   ./virtualisation/container-config.nix
   ./virtualisation/containerd.nix
@@ -1208,15 +1275,16 @@
   ./virtualisation/amazon-options.nix
   ./virtualisation/hyperv-guest.nix
   ./virtualisation/kvmgt.nix
+  ./virtualisation/openstack-options.nix
   ./virtualisation/openvswitch.nix
   ./virtualisation/parallels-guest.nix
   ./virtualisation/podman/default.nix
   ./virtualisation/qemu-guest-agent.nix
-  ./virtualisation/railcar.nix
   ./virtualisation/spice-usb-redirection.nix
   ./virtualisation/virtualbox-guest.nix
   ./virtualisation/virtualbox-host.nix
   ./virtualisation/vmware-guest.nix
+  ./virtualisation/vmware-host.nix
   ./virtualisation/waydroid.nix
   ./virtualisation/xen-dom0.nix
   ./virtualisation/xe-guest-utilities.nix
diff --git a/nixos/modules/profiles/all-hardware.nix b/nixos/modules/profiles/all-hardware.nix
index 25f68123a1da9..8347453d403b4 100644
--- a/nixos/modules/profiles/all-hardware.nix
+++ b/nixos/modules/profiles/all-hardware.nix
@@ -40,6 +40,9 @@ in
       # SD cards.
       "sdhci_pci"
 
+      # NVMe drives
+      "nvme"
+
       # Firewire support.  Not tested.
       "ohci1394" "sbp2"
 
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index 3c503fba2a390..a8601a9e2c067 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -99,6 +99,10 @@ with lib;
         stdenvNoCC # for runCommand
         busybox
         jq # for closureInfo
+        # For boot.initrd.systemd
+        makeInitrdNGTool
+        systemdStage1
+        systemdStage1Network
       ];
 
     # Show all debug messages from the kernel but don't log refused packets
diff --git a/nixos/modules/profiles/minimal.nix b/nixos/modules/profiles/minimal.nix
index e79b927238419..0e65989214a18 100644
--- a/nixos/modules/profiles/minimal.nix
+++ b/nixos/modules/profiles/minimal.nix
@@ -8,9 +8,6 @@ with lib;
 {
   environment.noXlibs = mkDefault true;
 
-  # This isn't perfect, but let's expect the user specifies an UTF-8 defaultLocale
-  i18n.supportedLocales = [ (config.i18n.defaultLocale + "/UTF-8") ];
-
   documentation.enable = mkDefault false;
 
   documentation.nixos.enable = mkDefault false;
diff --git a/nixos/modules/profiles/qemu-guest.nix b/nixos/modules/profiles/qemu-guest.nix
index d4335edfcf2d3..8b3df97ae0db9 100644
--- a/nixos/modules/profiles/qemu-guest.nix
+++ b/nixos/modules/profiles/qemu-guest.nix
@@ -1,13 +1,13 @@
 # Common configuration for virtual machines running under QEMU (using
 # virtio).
 
-{ ... }:
+{ config, lib, ... }:
 
 {
   boot.initrd.availableKernelModules = [ "virtio_net" "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_scsi" "9p" "9pnet_virtio" ];
   boot.initrd.kernelModules = [ "virtio_balloon" "virtio_console" "virtio_rng" ];
 
-  boot.initrd.postDeviceCommands =
+  boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable)
     ''
       # Set the system time from the hardware clock to work around a
       # bug in qemu-kvm > 1.5.2 (where the VM clock is initialised
diff --git a/nixos/modules/programs/_1password-gui.nix b/nixos/modules/programs/_1password-gui.nix
new file mode 100644
index 0000000000000..657116c267d92
--- /dev/null
+++ b/nixos/modules/programs/_1password-gui.nix
@@ -0,0 +1,65 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs._1password-gui;
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "programs" "_1password-gui" "gid" ] ''
+      A preallocated GID will be used instead.
+    '')
+  ];
+
+  options = {
+    programs._1password-gui = {
+      enable = mkEnableOption "the 1Password GUI application";
+
+      polkitPolicyOwners = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = literalExpression ''["user1" "user2" "user3"]'';
+        description = ''
+          A list of users who should be able to integrate 1Password with polkit-based authentication mechanisms.
+        '';
+      };
+
+      package = mkPackageOption pkgs "1Password GUI" {
+        default = [ "_1password-gui" ];
+      };
+    };
+  };
+
+  config =
+    let
+      package = cfg.package.override {
+        polkitPolicyOwners = cfg.polkitPolicyOwners;
+      };
+    in
+    mkIf cfg.enable {
+      environment.systemPackages = [ package ];
+      users.groups.onepassword.gid = config.ids.gids.onepassword;
+
+      security.wrappers = {
+        "1Password-BrowserSupport" = {
+          source = "${package}/share/1password/1Password-BrowserSupport";
+          owner = "root";
+          group = "onepassword";
+          setuid = false;
+          setgid = true;
+        };
+
+        "1Password-KeyringHelper" = {
+          source = "${package}/share/1password/1Password-KeyringHelper";
+          owner = "root";
+          group = "onepassword";
+          setuid = true;
+          setgid = true;
+        };
+      };
+
+    };
+}
diff --git a/nixos/modules/programs/_1password.nix b/nixos/modules/programs/_1password.nix
new file mode 100644
index 0000000000000..b87e9b776e85b
--- /dev/null
+++ b/nixos/modules/programs/_1password.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs._1password;
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "programs" "_1password" "gid" ] ''
+      A preallocated GID will be used instead.
+    '')
+  ];
+
+  options = {
+    programs._1password = {
+      enable = mkEnableOption "the 1Password CLI tool";
+
+      package = mkPackageOption pkgs "1Password CLI" {
+        default = [ "_1password" ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    users.groups.onepassword-cli.gid = config.ids.gids.onepassword-cli;
+
+    security.wrappers = {
+      "op" = {
+        source = "${cfg.package}/bin/op";
+        owner = "root";
+        group = "onepassword-cli";
+        setuid = false;
+        setgid = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/programs/adb.nix b/nixos/modules/programs/adb.nix
index 83bcfe886aa15..9e9e37f92a87d 100644
--- a/nixos/modules/programs/adb.nix
+++ b/nixos/modules/programs/adb.nix
@@ -23,8 +23,7 @@ with lib;
   ###### implementation
   config = mkIf config.programs.adb.enable {
     services.udev.packages = [ pkgs.android-udev-rules ];
-    # Give platform-tools lower priority so mke2fs+friends are taken from other packages first
-    environment.systemPackages = [ (lowPrio pkgs.androidenv.androidPkgs_9_0.platform-tools) ];
+    environment.systemPackages = [ pkgs.android-tools ];
     users.groups.adbusers = {};
   };
 }
diff --git a/nixos/modules/programs/atop.nix b/nixos/modules/programs/atop.nix
index ad75ab27666ce..a31078a891a0c 100644
--- a/nixos/modules/programs/atop.nix
+++ b/nixos/modules/programs/atop.nix
@@ -136,6 +136,24 @@ in
           packages = [ atop (lib.mkIf cfg.netatop.enable cfg.netatop.package) ];
           services =
             mkService cfg.atopService.enable "atop" [ atop ]
+            // lib.mkIf cfg.atopService.enable {
+              # always convert logs to newer version first
+              # XXX might trigger TimeoutStart but restarting atop.service will
+              # convert remainings logs and start eventually
+              atop.serviceConfig.ExecStartPre = pkgs.writeShellScript "atop-update-log-format" ''
+                set -e -u
+                for logfile in "$LOGPATH"/atop_*
+                do
+                  ${atop}/bin/atopconvert "$logfile" "$logfile".new
+                  # only replace old file if version was upgraded to avoid
+                  # false positives for atop-rotate.service
+                  if ! ${pkgs.diffutils}/bin/cmp -s "$logfile" "$logfile".new
+                  then
+                    ${pkgs.coreutils}/bin/mv -v -f "$logfile".new "$logfile"
+                  fi
+                done
+              '';
+            }
             // mkService cfg.atopacctService.enable "atopacct" [ atop ]
             // mkService cfg.netatop.enable "netatop" [ cfg.netatop.package ]
             // mkService cfg.atopgpu.enable "atopgpu" [ atop ];
diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix
index dc054504ea48c..1e5c6ff9b2440 100644
--- a/nixos/modules/programs/captive-browser.nix
+++ b/nixos/modules/programs/captive-browser.nix
@@ -1,8 +1,12 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
   cfg = config.programs.captive-browser;
+
+  inherit (lib)
+    concatStringsSep escapeShellArgs optionalString
+    literalExpression mkEnableOption mkIf mkOption mkOptionDefault types;
+
   browserDefault = chromium: concatStringsSep " " [
     ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
     ''${chromium}/bin/chromium''
@@ -15,6 +19,15 @@ let
     ''-no-default-browser-check''
     ''http://cache.nixos.org/''
   ];
+
+  desktopItem = pkgs.makeDesktopItem {
+    name = "captive-browser";
+    desktopName = "Captive Portal Browser";
+    exec = "/run/wrappers/bin/captive-browser";
+    icon = "nix-snowflake";
+    categories = [ "Network" ];
+  };
+
 in
 {
   ###### interface
@@ -84,6 +97,11 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    environment.systemPackages = [
+      (pkgs.runCommand "captive-browser-desktop-item" { } ''
+        install -Dm444 -t $out/share/applications ${desktopItem}/share/applications/*.desktop
+      '')
+    ];
 
     programs.captive-browser.dhcp-dns =
       let
diff --git a/nixos/modules/programs/cfs-zen-tweaks.nix b/nixos/modules/programs/cfs-zen-tweaks.nix
new file mode 100644
index 0000000000000..f168662bbefe7
--- /dev/null
+++ b/nixos/modules/programs/cfs-zen-tweaks.nix
@@ -0,0 +1,28 @@
+# CFS Zen Tweaks
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.cfs-zen-tweaks;
+
+in
+
+{
+
+  meta = {
+    maintainers = with maintainers; [ mkg20001 ];
+  };
+
+  options = {
+    programs.cfs-zen-tweaks.enable = mkEnableOption "CFS Zen Tweaks";
+  };
+
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.cfs-zen-tweaks ];
+
+    systemd.services.set-cfs-tweak.wantedBy = [ "multi-user.target" "suspend.target" "hibernate.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ];
+  };
+}
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
index 8a1653318ab5f..4b8bec33eb873 100644
--- a/nixos/modules/programs/chromium.nix
+++ b/nixos/modules/programs/chromium.nix
@@ -108,5 +108,8 @@ in
     # for google-chrome https://www.chromium.org/administrators/linux-quick-start
     environment.etc."opt/chrome/policies/managed/default.json".text = builtins.toJSON defaultProfile;
     environment.etc."opt/chrome/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
+    # for brave
+    environment.etc."brave/policies/managed/default.json".text = builtins.toJSON defaultProfile;
+    environment.etc."brave/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
   };
 }
diff --git a/nixos/modules/programs/clickshare.nix b/nixos/modules/programs/clickshare.nix
deleted file mode 100644
index 9980a7daf5254..0000000000000
--- a/nixos/modules/programs/clickshare.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-{
-
-  options.programs.clickshare-csc1.enable =
-    lib.options.mkEnableOption ''
-      Barco ClickShare CSC-1 driver/client.
-      This allows users in the <literal>clickshare</literal>
-      group to access and use a ClickShare USB dongle
-      that is connected to the machine
-    '';
-
-  config = lib.modules.mkIf config.programs.clickshare-csc1.enable {
-    environment.systemPackages = [ pkgs.clickshare-csc1 ];
-    services.udev.packages = [ pkgs.clickshare-csc1 ];
-    users.groups.clickshare = {};
-  };
-
-  meta.maintainers = [ lib.maintainers.yarny ];
-
-}
diff --git a/nixos/modules/programs/dmrconfig.nix b/nixos/modules/programs/dmrconfig.nix
index d2a5117c48ef2..89ac3443a1f33 100644
--- a/nixos/modules/programs/dmrconfig.nix
+++ b/nixos/modules/programs/dmrconfig.nix
@@ -6,7 +6,7 @@ let
   cfg = config.programs.dmrconfig;
 
 in {
-  meta.maintainers = [ maintainers.etu ];
+  meta.maintainers = with maintainers; [ ];
 
   ###### interface
   options = {
diff --git a/nixos/modules/programs/environment.nix b/nixos/modules/programs/environment.nix
index d552c751afd73..a448727be778f 100644
--- a/nixos/modules/programs/environment.nix
+++ b/nixos/modules/programs/environment.nix
@@ -40,13 +40,15 @@ in
         KDEDIRS = [ "" ];
         QT_PLUGIN_PATH = [ "/lib/qt4/plugins" "/lib/kde4/plugins" ];
         QTWEBKIT_PLUGIN_PATH = [ "/lib/mozilla/plugins/" ];
-        GTK_PATH = [ "/lib/gtk-2.0" "/lib/gtk-3.0" ];
+        GTK_PATH = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
         XDG_CONFIG_DIRS = [ "/etc/xdg" ];
         XDG_DATA_DIRS = [ "/share" ];
         MOZ_PLUGIN_PATH = [ "/lib/mozilla/plugins" ];
         LIBEXEC_PATH = [ "/lib/libexec" ];
       };
 
+    environment.pathsToLink = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
+
     environment.extraInit =
       ''
          unset ASPELL_CONF
diff --git a/nixos/modules/programs/firejail.nix b/nixos/modules/programs/firejail.nix
index 8c10d7c4df39f..76b42168c198a 100644
--- a/nixos/modules/programs/firejail.nix
+++ b/nixos/modules/programs/firejail.nix
@@ -17,8 +17,8 @@ let
         then value
         else { executable = value; profile = null; extraArgs = []; };
         args = lib.escapeShellArgs (
-          (optional (opts.profile != null) "--profile=${toString opts.profile}")
-          ++ opts.extraArgs
+          opts.extraArgs
+          ++ (optional (opts.profile != null) "--profile=${toString opts.profile}")
           );
       in
       ''
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index b41f30287ea55..7d8ab7dda9672 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -17,7 +17,7 @@ let
     else if xserverCfg.enable || config.programs.sway.enable then
       "gnome3"
     else
-      null;
+      "curses";
 
 in
 
diff --git a/nixos/modules/programs/haguichi.nix b/nixos/modules/programs/haguichi.nix
new file mode 100644
index 0000000000000..4f48551cf1dac
--- /dev/null
+++ b/nixos/modules/programs/haguichi.nix
@@ -0,0 +1,15 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+{
+  options.programs.haguichi = {
+    enable = mkEnableOption "Haguichi, a Linux GUI frontend to the proprietary LogMeIn Hamachi";
+  };
+
+  config = mkIf config.programs.haguichi.enable {
+    environment.systemPackages = with pkgs; [ haguichi ];
+
+    services.logmein-hamachi.enable = true;
+  };
+}
diff --git a/nixos/modules/programs/k3b.nix b/nixos/modules/programs/k3b.nix
new file mode 100644
index 0000000000000..68a4d08f349c2
--- /dev/null
+++ b/nixos/modules/programs/k3b.nix
@@ -0,0 +1,52 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  # interface
+  options.programs.k3b = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable k3b, the KDE disk burning application.
+
+        Additionally to installing <package>k3b</package> enabling this will
+        add <literal>setuid</literal> wrappers in <literal>/run/wrappers/bin</literal>
+        for both <package>cdrdao</package> and <package>cdrecord</package>. On first
+        run you must manually configure the path of <package>cdrdae</package> and
+        <package>cdrecord</package> to correspond to the appropriate paths under
+        <literal>/run/wrappers/bin</literal> in the "Setup External Programs" menu.
+      '';
+    };
+  };
+
+  # implementation
+  config = mkIf config.programs.k3b.enable {
+
+    environment.systemPackages = with pkgs; [
+      k3b
+      dvdplusrwtools
+      cdrdao
+      cdrkit
+    ];
+
+    security.wrappers = {
+      cdrdao = {
+        setuid = true;
+        owner = "root";
+        group = "cdrom";
+        permissions = "u+wrx,g+x";
+        source = "${pkgs.cdrdao}/bin/cdrdao";
+      };
+      cdrecord = {
+        setuid = true;
+        owner = "root";
+        group = "cdrom";
+        permissions = "u+wrx,g+x";
+        source = "${pkgs.cdrkit}/bin/cdrecord";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/programs/k40-whisperer.nix b/nixos/modules/programs/k40-whisperer.nix
new file mode 100644
index 0000000000000..3163e45f57e45
--- /dev/null
+++ b/nixos/modules/programs/k40-whisperer.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.k40-whisperer;
+  pkg = cfg.package.override {
+    udevGroup = cfg.group;
+  };
+in
+{
+  options.programs.k40-whisperer = {
+    enable = mkEnableOption "K40-Whisperer";
+
+    group = mkOption {
+      type = types.str;
+      description = ''
+        Group assigned to the device when connected.
+      '';
+      default = "k40";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.k40-whisperer;
+      defaultText = literalExpression "pkgs.k40-whisperer";
+      example = literalExpression "pkgs.k40-whisperer";
+      description = ''
+        K40 Whisperer package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.${cfg.group} = {};
+
+    environment.systemPackages = [ pkg ];
+    services.udev.packages = [ pkg ];
+  };
+}
diff --git a/nixos/modules/programs/kclock.nix b/nixos/modules/programs/kclock.nix
new file mode 100644
index 0000000000000..42d81d2798bad
--- /dev/null
+++ b/nixos/modules/programs/kclock.nix
@@ -0,0 +1,13 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.programs.kclock;
+  kclockPkg = pkgs.libsForQt5.kclock;
+in {
+  options.programs.kclock = { enable = mkEnableOption "Enable KClock"; };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ kclockPkg ];
+    environment.systemPackages = [ kclockPkg ];
+  };
+}
diff --git a/nixos/modules/programs/kdeconnect.nix b/nixos/modules/programs/kdeconnect.nix
index df698e84dd702..10d6e18a3d144 100644
--- a/nixos/modules/programs/kdeconnect.nix
+++ b/nixos/modules/programs/kdeconnect.nix
@@ -12,8 +12,8 @@ with lib;
       implementation if you use Gnome.
     '';
     package = mkOption {
-      default = pkgs.kdeconnect;
-      defaultText = literalExpression "pkgs.kdeconnect";
+      default = pkgs.plasma5Packages.kdeconnect-kde;
+      defaultText = literalExpression "pkgs.plasma5Packages.kdeconnect-kde";
       type = types.package;
       example = literalExpression "pkgs.gnomeExtensions.gsconnect";
       description = ''
diff --git a/nixos/modules/programs/mininet.nix b/nixos/modules/programs/mininet.nix
index 6e90e7669ac66..2cf6c014c3522 100644
--- a/nixos/modules/programs/mininet.nix
+++ b/nixos/modules/programs/mininet.nix
@@ -23,8 +23,8 @@ let
       ln -s ${pyEnv}/bin/mn $out/bin/mn
 
       # mn errors out without a telnet binary
-      # pkgs.telnet brings an undesired ifconfig into PATH see #43105
-      ln -s ${pkgs.telnet}/bin/telnet $out/bin/telnet
+      # pkgs.inetutils brings an undesired ifconfig into PATH see #43105
+      ln -s ${pkgs.inetutils}/bin/telnet $out/bin/telnet
     '';
 in
 {
diff --git a/nixos/modules/programs/nbd.nix b/nixos/modules/programs/nbd.nix
new file mode 100644
index 0000000000000..fea9bc1ff71a1
--- /dev/null
+++ b/nixos/modules/programs/nbd.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.nbd;
+in
+{
+  options = {
+    programs.nbd = {
+      enable = mkEnableOption "Network Block Device (nbd) support";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ nbd ];
+    boot.kernelModules = [ "nbd" ];
+  };
+}
diff --git a/nixos/modules/programs/nethoscope.nix b/nixos/modules/programs/nethoscope.nix
new file mode 100644
index 0000000000000..495548e9c6561
--- /dev/null
+++ b/nixos/modules/programs/nethoscope.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.nethoscope;
+in
+{
+  meta.maintainers = with maintainers; [ _0x4A6F ];
+
+  options = {
+    programs.nethoscope = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add nethoscope to the global environment and configure a
+          setcap wrapper for it.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ nethoscope ];
+    security.wrappers.nethoscope = {
+      source = "${pkgs.nethoscope}/bin/nethoscope";
+      capabilities = "cap_net_raw,cap_net_admin=eip";
+    };
+  };
+}
diff --git a/nixos/modules/programs/nix-ld.nix b/nixos/modules/programs/nix-ld.nix
new file mode 100644
index 0000000000000..89779cfef3909
--- /dev/null
+++ b/nixos/modules/programs/nix-ld.nix
@@ -0,0 +1,10 @@
+{ pkgs, lib, config, ... }:
+{
+  meta.maintainers = [ lib.maintainers.mic92 ];
+  options = {
+    programs.nix-ld.enable = lib.mkEnableOption ''nix-ld, Documentation: <link xlink:href="https://github.com/Mic92/nix-ld"/>'';
+  };
+  config = lib.mkIf config.programs.nix-ld.enable {
+    systemd.tmpfiles.packages = [ pkgs.nix-ld ];
+  };
+}
diff --git a/nixos/modules/programs/nncp.nix b/nixos/modules/programs/nncp.nix
new file mode 100644
index 0000000000000..29a703eadf10e
--- /dev/null
+++ b/nixos/modules/programs/nncp.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  nncpCfgFile = "/run/nncp.hjson";
+  programCfg = config.programs.nncp;
+  settingsFormat = pkgs.formats.json { };
+  jsonCfgFile = settingsFormat.generate "nncp.json" programCfg.settings;
+  pkg = programCfg.package;
+in {
+  options.programs.nncp = {
+
+    enable =
+      mkEnableOption "NNCP (Node to Node copy) utilities and configuration";
+
+    group = mkOption {
+      type = types.str;
+      default = "uucp";
+      description = ''
+        The group under which NNCP files shall be owned.
+        Any member of this group may access the secret keys
+        of this NNCP node.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.nncp;
+      defaultText = literalExpression "pkgs.nncp";
+      description = "The NNCP package to use system-wide.";
+    };
+
+    secrets = mkOption {
+      type = with types; listOf str;
+      example = [ "/run/keys/nncp.hjson" ];
+      description = ''
+        A list of paths to NNCP configuration files that should not be
+        in the Nix store. These files are layered on top of the values at
+        <xref linkend="opt-programs.nncp.settings"/>.
+      '';
+    };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      description = ''
+        NNCP configuration, see
+        <link xlink:href="http://www.nncpgo.org/Configuration.html"/>.
+        At runtime these settings will be overlayed by the contents of
+        <xref linkend="opt-programs.nncp.secrets"/> into the file
+        <literal>${nncpCfgFile}</literal>. Node keypairs go in
+        <literal>secrets</literal>, do not specify them in
+        <literal>settings</literal> as they will be leaked into
+        <literal>/nix/store</literal>!
+      '';
+      default = { };
+    };
+
+  };
+
+  config = mkIf programCfg.enable {
+
+    environment = {
+      systemPackages = [ pkg ];
+      etc."nncp.hjson".source = nncpCfgFile;
+    };
+
+    programs.nncp.settings = {
+      spool = mkDefault "/var/spool/nncp";
+      log = mkDefault "/var/spool/nncp/log";
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${programCfg.settings.spool} 0770 root ${programCfg.group}"
+      "f ${programCfg.settings.log} 0770 root ${programCfg.group}"
+    ];
+
+    systemd.services.nncp-config = {
+      path = [ pkg ];
+      description = "Generate NNCP configuration";
+      wantedBy = [ "basic.target" ];
+      serviceConfig.Type = "oneshot";
+      script = ''
+        umask u=rw
+        nncpCfgDir=$(mktemp --directory nncp.XXX)
+        for f in ${jsonCfgFile} ${toString config.programs.nncp.secrets}; do
+          tmpdir=$(mktemp --directory nncp.XXX)
+          nncp-cfgdir -cfg $f -dump $tmpdir
+          find $tmpdir -size 1c -delete
+          cp -a $tmpdir/* $nncpCfgDir/
+          rm -rf $tmpdir
+        done
+        nncp-cfgdir -load $nncpCfgDir > ${nncpCfgFile}
+        rm -rf $nncpCfgDir
+        chgrp ${programCfg.group} ${nncpCfgFile}
+        chmod g+r ${nncpCfgFile}
+      '';
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ehmry ];
+}
diff --git a/nixos/modules/programs/openvpn3.nix b/nixos/modules/programs/openvpn3.nix
new file mode 100644
index 0000000000000..f3101d3cebdc9
--- /dev/null
+++ b/nixos/modules/programs/openvpn3.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.openvpn3;
+in
+{
+  options.programs.openvpn3 = {
+    enable = mkEnableOption "the openvpn3 client";
+  };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = with pkgs; [
+      openvpn3
+    ];
+
+    users.users.openvpn = {
+      isSystemUser = true;
+      uid = config.ids.uids.openvpn;
+      group = "openvpn";
+    };
+
+    users.groups.openvpn = {
+      gid = config.ids.gids.openvpn;
+    };
+
+    environment.systemPackages = with pkgs; [
+      openvpn3
+    ];
+  };
+
+}
diff --git a/nixos/modules/programs/qt5ct.nix b/nixos/modules/programs/qt5ct.nix
index 88e861bf4031a..3ff47b355915b 100644
--- a/nixos/modules/programs/qt5ct.nix
+++ b/nixos/modules/programs/qt5ct.nix
@@ -1,31 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ lib, ... }:
 
 with lib;
 
 {
-  meta.maintainers = [ maintainers.romildo ];
-
-  ###### interface
-  options = {
-    programs.qt5ct = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Whether to enable the Qt5 Configuration Tool (qt5ct), a
-          program that allows users to configure Qt5 settings (theme,
-          font, icons, etc.) under desktop environments or window
-          manager without Qt integration.
-
-          Official home page: <link xlink:href="https://sourceforge.net/projects/qt5ct/">https://sourceforge.net/projects/qt5ct/</link>
-        '';
-      };
-    };
-  };
-
-  ###### implementation
-  config = mkIf config.programs.qt5ct.enable {
-    environment.variables.QT_QPA_PLATFORMTHEME = "qt5ct";
-    environment.systemPackages = with pkgs; [ libsForQt5.qt5ct ];
-  };
+  imports = [
+    (mkRemovedOptionModule [ "programs" "qt5ct" "enable" ] "Use qt5.platformTheme = \"qt5ct\" instead.")
+  ];
 }
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index b31fce9152404..75685de4f04e3 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -157,9 +157,13 @@ in
               default = [ name ] ++ config.extraHostNames;
               defaultText = literalExpression "[ ${name} ] ++ config.${options.extraHostNames}";
               description = ''
-                DEPRECATED, please use <literal>extraHostNames</literal>.
                 A list of host names and/or IP numbers used for accessing
-                the host's ssh service.
+                the host's ssh service. This list includes the name of the
+                containing <literal>knownHosts</literal> attribute by default
+                for convenience. If you wish to configure multiple host keys
+                for the same host use multiple <literal>knownHosts</literal>
+                entries with different attribute names and the same
+                <literal>hostNames</literal> list.
               '';
             };
             extraHostNames = mkOption {
@@ -167,7 +171,8 @@ in
               default = [];
               description = ''
                 A list of additional host names and/or IP numbers used for
-                accessing the host's ssh service.
+                accessing the host's ssh service. This list is ignored if
+                <literal>hostNames</literal> is set explicitly.
               '';
             };
             publicKey = mkOption {
@@ -198,7 +203,12 @@ in
           };
         }));
         description = ''
-          The set of system-wide known SSH hosts.
+          The set of system-wide known SSH hosts. To make simple setups more
+          convenient the name of an attribute in this set is used as a host name
+          for the entry. This behaviour can be disabled by setting
+          <literal>hostNames</literal> explicitly. You can use
+          <literal>extraHostNames</literal> to add additional host names without
+          disabling this default.
         '';
         example = literalExpression ''
           {
@@ -207,6 +217,10 @@ in
               publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub;
             };
             "myhost2.net".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIRuJ8p1Fi+m6WkHV0KWnRfpM1WxoW8XAS+XvsSKsTK";
+            "myhost2.net/dsa" = {
+              hostNames = [ "myhost2.net" ];
+              publicKeyFile = ./pubkeys/myhost2_ssh_host_dsa_key.pub;
+            };
           }
         '';
       };
@@ -279,9 +293,6 @@ in
         message = "knownHost ${name} must contain either a publicKey or publicKeyFile";
       });
 
-    warnings = mapAttrsToList (name: _: ''programs.ssh.knownHosts.${name}.hostNames is deprecated, use programs.ssh.knownHosts.${name}.extraHostNames'')
-      (filterAttrs (name: {hostNames, extraHostNames, ...}: hostNames != [ name ] ++ extraHostNames) cfg.knownHosts);
-
     # SSH configuration. Slight duplication of the sshd_config
     # generation in the sshd service.
     environment.etc."ssh/ssh_config".text =
diff --git a/nixos/modules/programs/ssmtp.nix b/nixos/modules/programs/ssmtp.nix
deleted file mode 100644
index b454bf35229ee..0000000000000
--- a/nixos/modules/programs/ssmtp.nix
+++ /dev/null
@@ -1,190 +0,0 @@
-# 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, without
-# queueing mail locally.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.ssmtp;
-
-in
-{
-
-  imports = [
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "directDelivery" ] [ "services" "ssmtp" "enable" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "hostName" ] [ "services" "ssmtp" "hostName" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "domain" ] [ "services" "ssmtp" "domain" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "root" ] [ "services" "ssmtp" "root" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "useTLS" ] [ "services" "ssmtp" "useTLS" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "useSTARTTLS" ] [ "services" "ssmtp" "useSTARTTLS" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "authUser" ] [ "services" "ssmtp" "authUser" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "authPassFile" ] [ "services" "ssmtp" "authPassFile" ])
-    (mkRenamedOptionModule [ "networking" "defaultMailServer" "setSendmail" ] [ "services" "ssmtp" "setSendmail" ])
-
-    (mkRemovedOptionModule [ "networking" "defaultMailServer" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
-    (mkRemovedOptionModule [ "services" "ssmtp" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
-  ];
-
-  options = {
-
-    services.ssmtp = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Use the trivial Mail Transfer Agent (MTA)
-          <command>ssmtp</command> package to allow programs to send
-          e-mail.  If you don't want to run a “real” MTA like
-          <command>sendmail</command> or <command>postfix</command> on
-          your machine, set this option to <literal>true</literal>, and
-          set the option
-          <option>services.ssmtp.hostName</option> to the
-          host name of your preferred mail server.
-        '';
-      };
-
-      settings = mkOption {
-        type = with types; attrsOf (oneOf [ bool str ]);
-        default = {};
-        description = ''
-          <citerefentry><refentrytitle>ssmtp</refentrytitle><manvolnum>5</manvolnum></citerefentry> configuration. Refer
-          to <link xlink:href="https://linux.die.net/man/5/ssmtp.conf"/> for details on supported values.
-        '';
-        example = literalExpression ''
-          {
-            Debug = true;
-            FromLineOverride = false;
-          }
-        '';
-      };
-
-      hostName = mkOption {
-        type = types.str;
-        example = "mail.example.org";
-        description = ''
-          The host name of the default mail server to use to deliver
-          e-mail. Can also contain a port number (ex: mail.example.org:587),
-          defaults to port 25 if no port is given.
-        '';
-      };
-
-      root = mkOption {
-        type = types.str;
-        default = "";
-        example = "root@example.org";
-        description = ''
-          The e-mail to which mail for users with UID &lt; 1000 is forwarded.
-        '';
-      };
-
-      domain = mkOption {
-        type = types.str;
-        default = "";
-        example = "example.org";
-        description = ''
-          The domain from which mail will appear to be sent.
-        '';
-      };
-
-      useTLS = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether TLS should be used to connect to the default mail
-          server.
-        '';
-      };
-
-      useSTARTTLS = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether the STARTTLS should be used to connect to the default
-          mail server.  (This is needed for TLS-capable mail servers
-          running on the default SMTP port 25.)
-        '';
-      };
-
-      authUser = mkOption {
-        type = types.str;
-        default = "";
-        example = "foo@example.org";
-        description = ''
-          Username used for SMTP auth. Leave blank to disable.
-        '';
-      };
-
-      authPassFile = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/run/keys/ssmtp-authpass";
-        description = ''
-          Path to a file that contains the password used for SMTP auth. The file
-          should not contain a trailing newline, if the password does not contain one
-          (e.g. use <command>echo -n "password" > file</command>).
-          This file should be readable by the users that need to execute ssmtp.
-        '';
-      };
-
-      setSendmail = mkOption {
-        type = types.bool;
-        default = true;
-        description = "Whether to set the system sendmail to ssmtp's.";
-      };
-
-    };
-
-  };
-
-
-  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;
-        FromLineOverride = mkDefault true;
-        UseTLS = cfg.useTLS;
-        UseSTARTTLS = cfg.useSTARTTLS;
-      })
-      (mkIf (cfg.root != "") { root = cfg.root; })
-      (mkIf (cfg.domain != "") { rewriteDomain = cfg.domain; })
-      (mkIf (cfg.authUser != "") { AuthUser = cfg.authUser; })
-      (mkIf (cfg.authPassFile != null) { AuthPassFile = cfg.authPassFile; })
-    ];
-
-    # 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 mkValueStringDefault {} value
-        ;
-      } "=";
-    } cfg.settings;
-
-    environment.systemPackages = [pkgs.ssmtp];
-
-    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
-      program = "sendmail";
-      source = "${pkgs.ssmtp}/bin/sendmail";
-      setuid = false;
-      setgid = false;
-      owner = "root";
-      group = "root";
-    };
-
-  };
-
-}
diff --git a/nixos/modules/programs/streamdeck-ui.nix b/nixos/modules/programs/streamdeck-ui.nix
new file mode 100644
index 0000000000000..1434f82660de3
--- /dev/null
+++ b/nixos/modules/programs/streamdeck-ui.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.streamdeck-ui;
+in {
+  options.programs.streamdeck-ui = {
+    enable = mkEnableOption "streamdeck-ui";
+
+    autoStart = mkOption {
+      default = true;
+      type = types.bool;
+      description = "Whether streamdeck-ui should be started automatically.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [
+      streamdeck-ui
+      (mkIf cfg.autoStart (makeAutostartItem { name = "streamdeck-ui"; package = streamdeck-ui; }))
+    ];
+
+    services.udev.packages = with pkgs; [ streamdeck-ui ];
+  };
+
+  meta.maintainers = with maintainers; [ majiir ];
+}
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index bb9904d195606..01b0472813447 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -134,6 +134,7 @@ in {
         '';
       };
     };
+    security.polkit.enable = true;
     security.pam.services.swaylock = {};
     hardware.opengl.enable = mkDefault true;
     fonts.enableDefaultFonts = mkDefault true;
diff --git a/nixos/modules/programs/thefuck.nix b/nixos/modules/programs/thefuck.nix
index b909916158d38..18d09e26866c1 100644
--- a/nixos/modules/programs/thefuck.nix
+++ b/nixos/modules/programs/thefuck.nix
@@ -6,9 +6,12 @@ let
   prg = config.programs;
   cfg = prg.thefuck;
 
-  initScript = ''
+  bashAndZshInitScript = ''
     eval $(${pkgs.thefuck}/bin/thefuck --alias ${cfg.alias})
   '';
+  fishInitScript = ''
+    ${pkgs.thefuck}/bin/thefuck --alias ${cfg.alias} | source
+  '';
 in
   {
     options = {
@@ -30,10 +33,8 @@ in
     config = mkIf cfg.enable {
       environment.systemPackages = with pkgs; [ thefuck ];
 
-      programs.bash.interactiveShellInit = initScript;
-      programs.zsh.interactiveShellInit = mkIf prg.zsh.enable initScript;
-      programs.fish.interactiveShellInit = mkIf prg.fish.enable ''
-        ${pkgs.thefuck}/bin/thefuck --alias | source
-      '';
+      programs.bash.interactiveShellInit = bashAndZshInitScript;
+      programs.zsh.interactiveShellInit = mkIf prg.zsh.enable bashAndZshInitScript;
+      programs.fish.interactiveShellInit = mkIf prg.fish.enable fishInitScript;
     };
   }
diff --git a/nixos/modules/programs/thunar.nix b/nixos/modules/programs/thunar.nix
new file mode 100644
index 0000000000000..5ea2982dd93cf
--- /dev/null
+++ b/nixos/modules/programs/thunar.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.thunar;
+
+in {
+  meta = {
+    maintainers = teams.xfce.members;
+  };
+
+  options = {
+    programs.thunar = {
+      enable = mkEnableOption "Thunar, the Xfce file manager";
+
+      plugins = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        description = "List of thunar plugins to install.";
+        example = literalExpression "with pkgs.xfce; [ thunar-archive-plugin thunar-volman ]";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable (
+    let package = pkgs.xfce.thunar.override { thunarPlugins = cfg.plugins; };
+
+    in {
+      environment.systemPackages = [
+        package
+      ];
+
+      services.dbus.packages = [
+        package
+      ];
+
+      systemd.packages = [
+        package
+      ];
+
+      programs.xfconf.enable = true;
+    }
+  );
+}
diff --git a/nixos/modules/programs/xfconf.nix b/nixos/modules/programs/xfconf.nix
new file mode 100644
index 0000000000000..8e854b40e513d
--- /dev/null
+++ b/nixos/modules/programs/xfconf.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.xfconf;
+
+in {
+  meta = {
+    maintainers = teams.xfce.members;
+  };
+
+  options = {
+    programs.xfconf = {
+      enable = mkEnableOption "Xfconf, the Xfce configuration storage system";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      pkgs.xfce.xfconf
+    ];
+
+    services.dbus.packages = [
+      pkgs.xfce.xfconf
+    ];
+  };
+}
diff --git a/nixos/modules/programs/zsh/zsh-autosuggestions.nix b/nixos/modules/programs/zsh/zsh-autosuggestions.nix
index fee324cc73264..2e53e907d547a 100644
--- a/nixos/modules/programs/zsh/zsh-autosuggestions.nix
+++ b/nixos/modules/programs/zsh/zsh-autosuggestions.nix
@@ -22,17 +22,18 @@ in
     };
 
     strategy = mkOption {
-      type = types.enum [ "history" "match_prev_cmd" ];
-      default = "history";
+      type = types.listOf (types.enum [ "history" "completion" "match_prev_cmd" ]);
+      default = [ "history" ];
       description = ''
-        Set ZSH_AUTOSUGGEST_STRATEGY to choose the strategy for generating suggestions.
-        There are currently two to choose from:
+        `ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated.
+        The strategies in the array are tried successively until a suggestion is found.
+        There are currently three built-in strategies to choose from:
 
-          * history: Chooses the most recent match.
-          * match_prev_cmd: Chooses the most recent match whose preceding history item matches
-            the most recently executed command (more info). Note that this strategy won't work as
-            expected with ZSH options that don't preserve the history order such as
-            HIST_IGNORE_ALL_DUPS or HIST_EXPIRE_DUPS_FIRST.
+        - `history`: Chooses the most recent match from history.
+        - `completion`: Chooses a suggestion based on what tab-completion would suggest. (requires `zpty` module)
+        - `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches
+            the most recently executed command. Note that this strategy won't work as expected with ZSH options that
+            don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`.
       '';
     };
 
@@ -62,7 +63,7 @@ in
       source ${pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh
 
       export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="${cfg.highlightStyle}"
-      export ZSH_AUTOSUGGEST_STRATEGY=("${cfg.strategy}")
+      export ZSH_AUTOSUGGEST_STRATEGY=(${concatStringsSep " " cfg.strategy})
       ${optionalString (!cfg.async) "unset ZSH_AUTOSUGGEST_USE_ASYNC"}
 
       ${concatStringsSep "\n" (mapAttrsToList (key: value: ''export ${key}="${value}"'') cfg.extraConfig)}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index c271d504b7716..22fcb72e9ff40 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -50,6 +50,7 @@ with lib;
     (mkRemovedOptionModule [ "services" "flashpolicyd" ] "The flashpolicyd module has been removed. Adobe Flash Player is deprecated.")
     (mkRemovedOptionModule [ "services" "fourStore" ] "The fourStore module has been removed")
     (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
+    (mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed")
     (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")
@@ -67,7 +68,13 @@ with lib;
       prey-bash-client is deprecated upstream
     '')
     (mkRemovedOptionModule [ "services" "quagga" ] "the corresponding package has been removed from nixpkgs")
+    (mkRemovedOptionModule [ "services" "railcar" ] "the corresponding package has been removed from nixpkgs")
     (mkRemovedOptionModule [ "services" "seeks" ] "")
+    (mkRemovedOptionModule [ "services" "ssmtp" ] ''
+      The ssmtp package and the corresponding module have been removed due to
+      the program being unmaintained. The options `programs.msmtp.*` can be
+      used instead.
+    '')
     (mkRemovedOptionModule [ "services" "venus" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "wakeonlan"] "This module was removed in favor of enabling it with networking.interfaces.<name>.wakeOnLan")
     (mkRemovedOptionModule [ "services" "winstone" ] "The corresponding package was removed from nixpkgs.")
@@ -87,10 +94,12 @@ with lib;
     (mkRemovedOptionModule [ "services" "racoon" ] ''
       The racoon module has been removed, because the software project was abandoned upstream.
     '')
-
     (mkRemovedOptionModule [ "services" "shellinabox" ] "The corresponding package was removed from nixpkgs.")
-
     (mkRemovedOptionModule [ "services" "gogoclient" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "virtuoso" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.")
 
     # Do NOT add any option renames here, see top of the file
   ];
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 9f295db84fd64..d9d072b36e6e6 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -61,6 +61,19 @@ let
         '';
       };
 
+      usshAuth = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, users with an SSH certificate containing an authorized principal
+          in their SSH agent are able to log in. Specific options are controlled
+          using the <option>security.pam.ussh</option> options.
+
+          Note that the  <option>security.pam.ussh.enable</option> must also be
+          set for this option to take effect.
+        '';
+      };
+
       yubicoAuth = mkOption {
         default = config.security.pam.yubico.enable;
         defaultText = literalExpression "config.security.pam.yubico.enable";
@@ -469,14 +482,18 @@ let
           (let p11 = config.security.pam.p11; in optionalString cfg.p11Auth ''
             auth ${p11.control} ${pkgs.pam_p11}/lib/security/pam_p11.so ${pkgs.opensc}/lib/opensc-pkcs11.so
           '') +
-          (let u2f = config.security.pam.u2f; in optionalString cfg.u2fAuth ''
-            auth ${u2f.control} ${pkgs.pam_u2f}/lib/security/pam_u2f.so ${optionalString u2f.debug "debug"} ${optionalString (u2f.authFile != null) "authfile=${u2f.authFile}"} ${optionalString u2f.interactive "interactive"} ${optionalString u2f.cue "cue"} ${optionalString (u2f.appId != null) "appid=${u2f.appId}"}
-          '') +
+          (let u2f = config.security.pam.u2f; in optionalString cfg.u2fAuth (''
+              auth ${u2f.control} ${pkgs.pam_u2f}/lib/security/pam_u2f.so ${optionalString u2f.debug "debug"} ${optionalString (u2f.authFile != null) "authfile=${u2f.authFile}"} ''
+                + ''${optionalString u2f.interactive "interactive"} ${optionalString u2f.cue "cue"} ${optionalString (u2f.appId != null) "appid=${u2f.appId}"} ${optionalString (u2f.origin != null) "origin=${u2f.origin}"}
+          '')) +
           optionalString cfg.usbAuth ''
             auth sufficient ${pkgs.pam_usb}/lib/security/pam_usb.so
           '' +
+          (let ussh = config.security.pam.ussh; in optionalString (config.security.pam.ussh.enable && cfg.usshAuth) ''
+            auth ${ussh.control} ${pkgs.pam_ussh}/lib/security/pam_ussh.so ${optionalString (ussh.caFile != null) "ca_file=${ussh.caFile}"} ${optionalString (ussh.authorizedPrincipals != null) "authorized_principals=${ussh.authorizedPrincipals}"} ${optionalString (ussh.authorizedPrincipalsFile != null) "authorized_principals_file=${ussh.authorizedPrincipalsFile}"} ${optionalString (ussh.group != null) "group=${ussh.group}"}
+          '') +
           (let oath = config.security.pam.oath; in optionalString cfg.oathAuth ''
-            auth requisite ${pkgs.oathToolkit}/lib/security/pam_oath.so window=${toString oath.window} usersfile=${toString oath.usersFile} digits=${toString oath.digits}
+            auth requisite ${pkgs.oath-toolkit}/lib/security/pam_oath.so window=${toString oath.window} usersfile=${toString oath.usersFile} digits=${toString oath.digits}
           '') +
           (let yubi = config.security.pam.yubico; in optionalString cfg.yubicoAuth ''
             auth ${yubi.control} ${pkgs.yubico-pam}/lib/security/pam_yubico.so mode=${toString yubi.mode} ${optionalString (yubi.challengeResponsePath != null) "chalresp_path=${yubi.challengeResponsePath}"} ${optionalString (yubi.mode == "client") "id=${toString yubi.id}"} ${optionalString yubi.debug "debug"}
@@ -518,7 +535,7 @@ let
                 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
+                auth required ${pkgs.google-authenticator}/lib/security/pam_google_authenticator.so no_increment_hotp
               '' +
               optionalString cfg.duoSecurity.enable ''
                 auth required ${pkgs.duo-unix}/lib/security/pam_duo.so
@@ -609,7 +626,7 @@ let
             session optional ${pkgs.otpw}/lib/security/pam_otpw.so
           '' +
           optionalString cfg.startSession ''
-            session optional ${pkgs.systemd}/lib/security/pam_systemd.so
+            session optional ${config.systemd.package}/lib/security/pam_systemd.so
           '' +
           optionalString cfg.forwardXAuth ''
             session optional pam_xauth.so xauthpath=${pkgs.xorg.xauth}/bin/xauth systemuser=99
@@ -878,6 +895,24 @@ in
         '';
       };
 
+      origin = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+            By default <literal>pam-u2f</literal> module sets the origin
+            to <literal>pam://$HOSTNAME</literal>.
+            Setting origin to an host independent value will allow you to
+            reuse credentials across machines
+
+            When using <command>pamu2fcfg</command>, you can specify your
+            application ID with the <literal>-o</literal> flag.
+
+            More information can be found <link
+            xlink:href="https://developers.yubico.com/pam-u2f/Manuals/pam_u2f.8.html">
+            here</link>
+        '';
+      };
+
       control = mkOption {
         default = "sufficient";
         type = types.enum [ "required" "requisite" "sufficient" "optional" ];
@@ -926,6 +961,96 @@ in
       };
     };
 
+    security.pam.ussh = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables Uber's USSH PAM (<literal>pam-ussh</literal>) module.
+
+          This is similar to <literal>pam-ssh-agent</literal>, except that
+          the presence of a CA-signed SSH key with a valid principal is checked
+          instead.
+
+          Note that this module must both be enabled using this option and on a
+          per-PAM-service level as well (using <literal>usshAuth</literal>).
+
+          More information can be found <link
+          xlink:href="https://github.com/uber/pam-ussh">here</link>.
+        '';
+      };
+
+      caFile = mkOption {
+        default = null;
+        type = with types; nullOr path;
+        description = ''
+          By default <literal>pam-ussh</literal> reads the trusted user CA keys
+          from <filename>/etc/ssh/trusted_user_ca</filename>.
+
+          This should be set the same as your <literal>TrustedUserCAKeys</literal>
+          option for sshd.
+        '';
+      };
+
+      authorizedPrincipals = mkOption {
+        default = null;
+        type = with types; nullOr commas;
+        description = ''
+          Comma-separated list of authorized principals to permit; if the user
+          presents a certificate with one of these principals, then they will be
+          authorized.
+
+          Note that <literal>pam-ussh</literal> also requires that the certificate
+          contain a principal matching the user's username. The principals from
+          this list are in addition to those principals.
+
+          Mutually exclusive with <literal>authorizedPrincipalsFile</literal>.
+        '';
+      };
+
+      authorizedPrincipalsFile = mkOption {
+        default = null;
+        type = with types; nullOr path;
+        description = ''
+          Path to a list of principals; if the user presents a certificate with
+          one of these principals, then they will be authorized.
+
+          Note that <literal>pam-ussh</literal> also requires that the certificate
+          contain a principal matching the user's username. The principals from
+          this file are in addition to those principals.
+
+          Mutually exclusive with <literal>authorizedPrincipals</literal>.
+        '';
+      };
+
+      group = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+          If set, then the authenticating user must be a member of this group
+          to use this module.
+        '';
+      };
+
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use the SSH certificate instead of the regular password,
+          use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+    };
+
     security.pam.yubico = {
       enable = mkOption {
         default = false;
@@ -1024,7 +1149,7 @@ in
       ++ optional config.services.sssd.enable pkgs.sssd
       ++ optionals config.krb5.enable [pam_krb5 pam_ccreds]
       ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
-      ++ optionals config.security.pam.oath.enable [ pkgs.oathToolkit ]
+      ++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ]
       ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ]
       ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
 
@@ -1110,8 +1235,11 @@ in
       optionalString (isEnabled (cfg: cfg.usbAuth)) ''
         mr ${pkgs.pam_usb}/lib/security/pam_usb.so,
       '' +
+      optionalString (isEnabled (cfg: cfg.usshAuth)) ''
+        mr ${pkgs.pam_ussh}/lib/security/pam_ussh.so,
+      '' +
       optionalString (isEnabled (cfg: cfg.oathAuth)) ''
-        "mr ${pkgs.oathToolkit}/lib/security/pam_oath.so,
+        "mr ${pkgs.oath-toolkit}/lib/security/pam_oath.so,
       '' +
       optionalString (isEnabled (cfg: cfg.yubicoAuth)) ''
         mr ${pkgs.yubico-pam}/lib/security/pam_yubico.so,
@@ -1132,7 +1260,7 @@ in
         mr ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so,
       '' +
       optionalString (isEnabled (cfg: cfg.startSession)) ''
-        mr ${pkgs.systemd}/lib/security/pam_systemd.so,
+        mr ${config.systemd.package}/lib/security/pam_systemd.so,
       '' +
       optionalString (isEnabled (cfg: cfg.enableAppArmor)
                      && config.security.apparmor.enable) ''
diff --git a/nixos/modules/security/pam_mount.nix b/nixos/modules/security/pam_mount.nix
index 462b7f89e2f48..1d0efee8ca8ee 100644
--- a/nixos/modules/security/pam_mount.nix
+++ b/nixos/modules/security/pam_mount.nix
@@ -5,6 +5,14 @@ with lib;
 let
   cfg = config.security.pam.mount;
 
+  oflRequired = cfg.logoutHup || cfg.logoutTerm || cfg.logoutKill;
+
+  fake_ofl = pkgs.writeShellScriptBin "fake_ofl" ''
+    SIGNAL=$1
+    MNTPT=$2
+    ${pkgs.lsof}/bin/lsof | ${pkgs.gnugrep}/bin/grep $MNTPT | ${pkgs.gawk}/bin/awk '{print $2}' | ${pkgs.findutils}/bin/xargs ${pkgs.util-linux}/bin/kill -$SIGNAL
+  '';
+
   anyPamMount = any (attrByPath ["pamMount"] false) (attrValues config.security.pam.services);
 in
 
@@ -51,6 +59,71 @@ in
           You can define volume-specific options in the volume definitions.
         '';
       };
+
+      debugLevel = mkOption {
+        type = types.int;
+        default = 0;
+        example = 1;
+        description = ''
+          Sets the Debug-Level. 0 disables debugging, 1 enables pam_mount tracing,
+          and 2 additionally enables tracing in mount.crypt. The default is 0.
+          For more information, visit <link
+          xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
+        '';
+      };
+
+      logoutWait = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Amount of microseconds to wait until killing remaining processes after
+          final logout.
+          For more information, visit <link
+          xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
+        '';
+      };
+
+      logoutHup = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Kill remaining processes after logout by sending a SIGHUP.
+        '';
+      };
+
+      logoutTerm = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Kill remaining processes after logout by sending a SIGTERM.
+        '';
+      };
+
+      logoutKill = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Kill remaining processes after logout by sending a SIGKILL.
+        '';
+      };
+
+      createMountPoints = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Create mountpoints for volumes if they do not exist.
+        '';
+      };
+
+      removeCreatedMountPoints = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Remove mountpoints created by pam_mount after logout. This
+          only affects mountpoints that have been created by pam_mount
+          in the same session.
+        '';
+      };
     };
 
   };
@@ -77,21 +150,20 @@ in
           <!DOCTYPE pam_mount SYSTEM "pam_mount.conf.xml.dtd">
           <!-- auto generated from Nixos: modules/config/users-groups.nix -->
           <pam_mount>
-          <debug enable="0" />
-
+          <debug enable="${toString cfg.debugLevel}" />
           <!-- if activated, requires ofl from hxtools to be present -->
-          <logout wait="0" hup="no" term="no" kill="no" />
+          <logout wait="${toString cfg.logoutWait}" hup="${if cfg.logoutHup then "yes" else "no"}" term="${if cfg.logoutTerm then "yes" else "no"}" kill="${if cfg.logoutKill then "yes" else "no"}" />
           <!-- set PATH variable for pam_mount module -->
           <path>${makeBinPath ([ pkgs.util-linux ] ++ cfg.additionalSearchPaths)}</path>
           <!-- create mount point if not present -->
-          <mkmountpoint enable="1" remove="true" />
-
+          <mkmountpoint enable="${if cfg.createMountPoints then "1" else "0"}" remove="${if cfg.removeCreatedMountPoints then "true" else "false"}" />
           <!-- specify the binaries to be called -->
           <fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ${concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])}</fusemount>
+          <fuseumount>${pkgs.fuse}/bin/fusermount -u %(MNTPT)</fuseumount>
           <cryptmount>${pkgs.pam_mount}/bin/mount.crypt %(VOLUME) %(MNTPT)</cryptmount>
           <cryptumount>${pkgs.pam_mount}/bin/umount.crypt %(MNTPT)</cryptumount>
           <pmvarrun>${pkgs.pam_mount}/bin/pmvarrun -u %(USER) -o %(OPERATION)</pmvarrun>
-
+          ${optionalString oflRequired "<ofl>${fake_ofl}/bin/fake_ofl %(SIGNAL) %(MNTPT)</ofl>"}
           ${concatStrings (map userVolumeEntry (attrValues extraUserVolumes))}
           ${concatStringsSep "\n" cfg.extraVolumes}
           </pam_mount>
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
index d9c58152f1faa..1ba149745c654 100644
--- a/nixos/modules/security/polkit.nix
+++ b/nixos/modules/security/polkit.nix
@@ -12,11 +12,7 @@ in
 
   options = {
 
-    security.polkit.enable = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Whether to enable PolKit.";
-    };
+    security.polkit.enable = mkEnableOption "polkit";
 
     security.polkit.extraConfig = mkOption {
       type = types.lines;
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index 99e578f8adae6..4bf239fca8f90 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -245,7 +245,7 @@ in
 
     environment.systemPackages = [ sudo ];
 
-    security.pam.services.sudo = { sshAgentAuth = true; };
+    security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; };
 
     environment.etc.sudoers =
       { source =
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index 0e3ec5af323ee..07b725effb7d1 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -22,16 +22,17 @@ in {
       options.confinement.fullUnit = lib.mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = lib.mdDoc ''
           Whether to include the full closure of the systemd unit file into the
           chroot, instead of just the dependencies for the executables.
 
-          <warning><para>While it may be tempting to just enable this option to
+          ::: {.warning}
+          While it may be tempting to just enable this option to
           make things work quickly, please be aware that this might add paths
           to the closure of the chroot that you didn't anticipate. It's better
-          to use <option>confinement.packages</option> to <emphasis
-          role="strong">explicitly</emphasis> add additional store paths to the
-          chroot.</para></warning>
+          to use {option}`confinement.packages` to **explicitly** add additional store paths to the
+          chroot.
+          :::
         '';
       };
 
@@ -175,8 +176,8 @@ in {
       serviceName = "${name}.service";
       excludedPath = rootPaths;
     } ''
-      mkdir -p "$out/lib/systemd/system"
-      serviceFile="$out/lib/systemd/system/$serviceName"
+      mkdir -p "$out/lib/systemd/system/$serviceName.d"
+      serviceFile="$out/lib/systemd/system/$serviceName.d/confinement.conf"
 
       echo '[Service]' > "$serviceFile"
 
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix
index 66a47bcaab6c9..169ef7442626e 100644
--- a/nixos/modules/security/wrappers/default.nix
+++ b/nixos/modules/security/wrappers/default.nix
@@ -92,14 +92,13 @@ let
     , permissions
     , ...
     }:
-    assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
     ''
       cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
       echo -n "${source}" > "$wrapperDir/${program}.real"
 
       # Prevent races
       chmod 0000 "$wrapperDir/${program}"
-      chown ${owner}.${group} "$wrapperDir/${program}"
+      chown ${owner}:${group} "$wrapperDir/${program}"
 
       # Set desired capabilities on the file plus cap_setpcap so
       # the wrapper program can elevate the capabilities set on
@@ -127,7 +126,7 @@ let
 
       # Prevent races
       chmod 0000 "$wrapperDir/${program}"
-      chown ${owner}.${group} "$wrapperDir/${program}"
+      chown ${owner}:${group} "$wrapperDir/${program}"
 
       chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
     '';
diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c
index 529669facda87..a21ec500208df 100644
--- a/nixos/modules/security/wrappers/wrapper.c
+++ b/nixos/modules/security/wrappers/wrapper.c
@@ -2,12 +2,12 @@
 #include <stdio.h>
 #include <string.h>
 #include <unistd.h>
+#include <stdnoreturn.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/prctl.h>
@@ -16,10 +16,7 @@
 #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
-// loudly if they are violated.
-#undef NDEBUG
+#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
 
 extern char **environ;
 
@@ -38,6 +35,12 @@ static char *wrapper_debug = "WRAPPER_DEBUG";
 #define LE32_TO_H(x) (x)
 #endif
 
+static noreturn void assert_failure(const char *assertion) {
+    fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
+    fflush(stderr);
+    abort();
+}
+
 int get_last_cap(unsigned *last_cap) {
     FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
     if (file == NULL) {
@@ -167,6 +170,7 @@ int readlink_malloc(const char *p, char **ret) {
 }
 
 int main(int argc, char **argv) {
+    ASSERT(argc >= 1);
     char *self_path = NULL;
     int self_path_size = readlink_malloc("/proc/self/exe", &self_path);
     if (self_path_size < 0) {
@@ -181,36 +185,36 @@ int main(int argc, char **argv) {
     int len = strlen(wrapper_dir);
     if (len > 0 && '/' == wrapper_dir[len - 1])
       --len;
-    assert(!strncmp(self_path, wrapper_dir, len));
-    assert('/' == wrapper_dir[0]);
-    assert('/' == self_path[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
     // `self_path', and not, say, as some other setuid program. That
     // is, our effective uid/gid should match the uid/gid of
     // `self_path'.
     struct stat st;
-    assert(lstat(self_path, &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()));
+    ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
+    ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
 
     // And, of course, we shouldn't be writable.
-    assert(!(st.st_mode & (S_IWGRP | S_IWOTH)));
+    ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
 
     // Read the path of the real (wrapped) program from <self>.real.
     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));
+    ASSERT(real_fn_size < sizeof(real_fn));
 
     int fd_self = open(real_fn, O_RDONLY);
-    assert(fd_self != -1);
+    ASSERT(fd_self != -1);
 
     char source_prog[PATH_MAX];
     len = read(fd_self, source_prog, PATH_MAX);
-    assert(len != -1);
-    assert(len < sizeof(source_prog));
-    assert(len > 0);
+    ASSERT(len != -1);
+    ASSERT(len < sizeof(source_prog));
+    ASSERT(len > 0);
     source_prog[len] = 0;
 
     close(fd_self);
diff --git a/nixos/modules/services/admin/pgadmin.nix b/nixos/modules/services/admin/pgadmin.nix
new file mode 100644
index 0000000000000..80b6814541045
--- /dev/null
+++ b/nixos/modules/services/admin/pgadmin.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  pkg = pkgs.pgadmin4;
+  cfg = config.services.pgadmin;
+
+  _base = with types; [ int bool str ];
+  base = with types; oneOf ([ (listOf (oneOf _base)) (attrsOf (oneOf _base)) ] ++ _base);
+
+  formatAttrset = attr:
+    "{${concatStringsSep "\n" (mapAttrsToList (key: value: "${builtins.toJSON key}: ${formatPyValue value},") attr)}}";
+
+  formatPyValue = value:
+    if builtins.isString value then builtins.toJSON value
+    else if value ? _expr then value._expr
+    else if builtins.isInt value then toString value
+    else if builtins.isBool value then (if value then "True" else "False")
+    else if builtins.isAttrs value then (formatAttrset value)
+    else if builtins.isList value then "[${concatStringsSep "\n" (map (v: "${formatPyValue v},") value)}]"
+    else throw "Unrecognized type";
+
+  formatPy = attrs:
+    concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${formatPyValue value}") attrs);
+
+  pyType = with types; attrsOf (oneOf [ (attrsOf base) (listOf base) base ]);
+in
+{
+  options.services.pgadmin = {
+    enable = mkEnableOption "PostgreSQL Admin 4";
+
+    port = mkOption {
+      description = "Port for pgadmin4 to run on";
+      type = types.port;
+      default = 5050;
+    };
+
+    initialEmail = mkOption {
+      description = "Initial email for the pgAdmin account.";
+      type = types.str;
+    };
+
+    initialPasswordFile = mkOption {
+      description = ''
+        Initial password file for the pgAdmin account.
+        NOTE: Should be string not a store path, to prevent the password from being world readable.
+      '';
+      type = types.path;
+    };
+
+    openFirewall = mkEnableOption "firewall passthrough for pgadmin4";
+
+    settings = mkOption {
+      description = ''
+        Settings for pgadmin4.
+        <link xlink:href="https://www.pgadmin.org/docs/pgadmin4/development/config_py.html">Documentation</link>.
+      '';
+      type = pyType;
+      default= {};
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.pgadmin.settings = {
+      DEFAULT_SERVER_PORT = cfg.port;
+      SERVER_MODE = true;
+    } // (optionalAttrs cfg.openFirewall {
+      DEFAULT_SERVER = mkDefault "::";
+    });
+
+    systemd.services.pgadmin = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      requires = [ "network.target" ];
+      # we're adding this optionally so just in case there's any race it'll be caught
+      # in case postgres doesn't start, pgadmin will just start normally
+      wants = [ "postgresql.service" ];
+
+      path = [ config.services.postgresql.package pkgs.coreutils pkgs.bash ];
+
+      preStart = ''
+        # NOTE: this is idempotent (aka running it twice has no effect)
+        (
+          # Email address:
+          echo ${escapeShellArg cfg.initialEmail}
+
+          # file might not contain newline. echo hack fixes that.
+          PW=$(cat ${escapeShellArg cfg.initialPasswordFile})
+
+          # Password:
+          echo "$PW"
+          # Retype password:
+          echo "$PW"
+        ) | ${pkg}/bin/pgadmin4-setup
+      '';
+
+      restartTriggers = [
+        "/etc/pgadmin/config_system.py"
+      ];
+
+      serviceConfig = {
+        User = "pgadmin";
+        DynamicUser = true;
+        LogsDirectory = "pgadmin";
+        StateDirectory = "pgadmin";
+        ExecStart = "${pkg}/bin/pgadmin4";
+      };
+    };
+
+    users.users.pgadmin = {
+      isSystemUser = true;
+      group = "pgadmin";
+    };
+
+    users.groups.pgadmin = {};
+
+    environment.etc."pgadmin/config_system.py" = {
+      text = formatPy cfg.settings;
+      mode = "0600";
+      user = "pgadmin";
+      group = "pgadmin";
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
index f573bd2ab8dd0..e0f8d461af076 100644
--- a/nixos/modules/services/audio/jmusicbot.nix
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -9,6 +9,13 @@ in
     services.jmusicbot = {
       enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jmusicbot;
+        defaultText = literalExpression "pkgs.jmusicbot";
+        description = "JMusicBot package to use";
+      };
+
       stateDir = mkOption {
         type = types.path;
         description = ''
@@ -27,7 +34,7 @@ in
       after = [ "network-online.target" ];
       description = "Discord music bot that's easy to set up and run yourself!";
       serviceConfig = mkMerge [{
-        ExecStart = "${pkgs.jmusicbot}/bin/JMusicBot";
+        ExecStart = "${cfg.package}/bin/JMusicBot";
         WorkingDirectory = cfg.stateDir;
         Restart = "always";
         RestartSec = 20;
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index 586b9ffa68888..11733d99fca65 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -215,6 +215,7 @@ in {
     systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
       wantedBy = [ "sockets.target" ];
       listenStreams = [
+        ""  # Note: this is needed to override the upstream unit
         (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
           then cfg.network.listenAddress
           else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index 3660e05310be1..319212c020777 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -45,7 +45,10 @@ in {
         RootDirectory = "/run/navidrome";
         ReadWritePaths = "";
         BindReadOnlyPaths = [
+          # navidrome uses online services to download additional album metadata / covers
+          "${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt"
           builtins.storeDir
+          "/etc"
         ] ++ lib.optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder;
         CapabilityBoundingSet = "";
         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index b82aca3976f0c..91d97a0b551e2 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, options, lib, pkgs, ... }:
 
 with lib;
 
@@ -44,24 +44,24 @@ let
 
   optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
     # global options
-    ++ [ "--stream.bind_to_address ${cfg.listenAddress}" ]
-    ++ [ "--stream.port ${toString cfg.port}" ]
-    ++ optionalNull cfg.sampleFormat "--stream.sampleformat ${cfg.sampleFormat}"
-    ++ optionalNull cfg.codec "--stream.codec ${cfg.codec}"
-    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${toString cfg.streamBuffer}"
-    ++ optionalNull cfg.buffer "--stream.buffer ${toString cfg.buffer}"
+    ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
+    ++ [ "--stream.port=${toString cfg.port}" ]
+    ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
+    ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
+    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${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}" ]
+    ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
     ++ optionals cfg.tcp.enable [
-      "--tcp.bind_to_address ${cfg.tcp.listenAddress}"
-      "--tcp.port ${toString cfg.tcp.port}" ]
+      "--tcp.bind_to_address=${cfg.tcp.listenAddress}"
+      "--tcp.port=${toString cfg.tcp.port}" ]
      # http json rpc
-    ++ [ "--http.enabled ${toString cfg.http.enable}" ]
+    ++ [ "--http.enabled=${toString cfg.http.enable}" ]
     ++ optionals cfg.http.enable [
-      "--http.bind_to_address ${cfg.http.listenAddress}"
-      "--http.port ${toString cfg.http.port}"
-    ] ++ optional (cfg.http.docRoot != null) "--http.doc_root \"${toString cfg.http.docRoot}\"");
+      "--http.bind_to_address=${cfg.http.listenAddress}"
+      "--http.port=${toString cfg.http.port}"
+    ] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
 
 in {
   imports = [
@@ -101,6 +101,8 @@ in {
 
       openFirewall = mkOption {
         type = types.bool;
+        # Make the behavior consistent with other services. Set the default to
+        # false and remove the accompanying warning after NixOS 22.05 is released.
         default = true;
         description = ''
           Whether to automatically open the specified ports in the firewall.
@@ -273,10 +275,16 @@ 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);
+    warnings =
+      # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
+      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)
+      # Remove this warning after NixOS 22.05 is released.
+      ++ optional (options.services.snapserver.openFirewall.highestPrio >= (mkOptionDefault null).priority) ''
+        services.snapserver.openFirewall will no longer default to true starting with NixOS 22.11.
+        Enable it explicitly if you need to control Snapserver remotely.
+      '';
 
     systemd.services.snapserver = {
       after = [ "network.target" ];
@@ -304,8 +312,8 @@ in {
 
     networking.firewall.allowedTCPPorts =
       optionals cfg.openFirewall [ cfg.port ]
-      ++ optional cfg.tcp.enable cfg.tcp.port
-      ++ optional cfg.http.enable cfg.http.port;
+      ++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
+      ++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
   };
 
   meta = {
diff --git a/nixos/modules/services/audio/squeezelite.nix b/nixos/modules/services/audio/squeezelite.nix
index 05506f5bcc7ac..36295e21c60f9 100644
--- a/nixos/modules/services/audio/squeezelite.nix
+++ b/nixos/modules/services/audio/squeezelite.nix
@@ -1,50 +1,46 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
+
   dataDir = "/var/lib/squeezelite";
   cfg = config.services.squeezelite;
+  pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite;
+  bin = "${pkg}/bin/${pkg.pname}";
 
-in {
+in
+{
 
   ###### interface
 
-  options = {
-
-    services.squeezelite= {
+  options.services.squeezelite = {
+    enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
 
-      enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
-
-      extraArguments = mkOption {
-        default = "";
-        type = types.str;
-        description = ''
-          Additional command line arguments to pass to Squeezelite.
-        '';
-      };
+    pulseAudio = mkEnableOption "pulseaudio support";
 
+    extraArguments = mkOption {
+      default = "";
+      type = types.str;
+      description = ''
+        Additional command line arguments to pass to Squeezelite.
+      '';
     };
-
   };
 
 
   ###### implementation
 
   config = mkIf cfg.enable {
-
-    systemd.services.squeezelite= {
+    systemd.services.squeezelite = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" "sound.target" ];
       description = "Software Squeezebox emulator";
       serviceConfig = {
         DynamicUser = true;
-        ExecStart = "${pkgs.squeezelite}/bin/squeezelite -N ${dataDir}/player-name ${cfg.extraArguments}";
+        ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}";
         StateDirectory = builtins.baseNameOf dataDir;
         SupplementaryGroups = "audio";
       };
     };
-
   };
-
 }
diff --git a/nixos/modules/services/backup/automysqlbackup.nix b/nixos/modules/services/backup/automysqlbackup.nix
index fd2764a40ad2f..cf0cb4da32cf1 100644
--- a/nixos/modules/services/backup/automysqlbackup.nix
+++ b/nixos/modules/services/backup/automysqlbackup.nix
@@ -112,7 +112,7 @@ in
 
     services.mysql.ensureUsers = optional (config.services.mysql.enable && cfg.config.mysql_dump_host == "localhost") {
       name = user;
-      ensurePermissions = { "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES"; };
+      ensurePermissions = { "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT"; };
     };
 
   };
diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix
index 5e5c0bbecccaa..9414d78aa751d 100644
--- a/nixos/modules/services/backup/borgmatic.nix
+++ b/nixos/modules/services/backup/borgmatic.nix
@@ -4,7 +4,8 @@ with lib;
 
 let
   cfg = config.services.borgmatic;
-  cfgfile = pkgs.writeText "config.yaml" (builtins.toJSON cfg.settings);
+  settingsFormat = pkgs.formats.yaml { };
+  cfgfile = settingsFormat.generate "config.yaml" cfg.settings;
 in {
   options.services.borgmatic = {
     enable = mkEnableOption "borgmatic";
@@ -14,7 +15,7 @@ in {
         See https://torsion.org/borgmatic/docs/reference/configuration/
       '';
       type = types.submodule {
-        freeformType = with lib.types; attrsOf anything;
+        freeformType = settingsFormat.type;
         options.location = {
           source_directories = mkOption {
             type = types.listOf types.str;
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
index 0c00b93440506..e17761ffc3cb9 100644
--- a/nixos/modules/services/backup/btrbk.nix
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -1,18 +1,36 @@
 { config, pkgs, lib, ... }:
 let
+  inherit (lib)
+    concatMapStringsSep
+    concatStringsSep
+    filterAttrs
+    flatten
+    isAttrs
+    isString
+    literalExpression
+    mapAttrs'
+    mapAttrsToList
+    mkIf
+    mkOption
+    optionalString
+    partition
+    typeOf
+    types
+    ;
+
   cfg = config.services.btrbk;
   sshEnabled = cfg.sshAccess != [ ];
   serviceEnabled = cfg.instances != { };
   attr2Lines = attr:
     let
-      pairs = lib.attrsets.mapAttrsToList (name: value: { inherit name value; }) attr;
+      pairs = mapAttrsToList (name: value: { inherit name value; }) attr;
       isSubsection = value:
-        if builtins.isAttrs value then true
-        else if builtins.isString value then false
-        else throw "invalid type in btrbk config ${builtins.typeOf value}";
-      sortedPairs = lib.lists.partition (x: isSubsection x.value) pairs;
+        if isAttrs value then true
+        else if isString value then false
+        else throw "invalid type in btrbk config ${typeOf value}";
+      sortedPairs = partition (x: isSubsection x.value) pairs;
     in
-    lib.flatten (
+    flatten (
       # non subsections go first
       (
         map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
@@ -22,7 +40,7 @@ let
         map
           (
             pair:
-            lib.mapAttrsToList
+            mapAttrsToList
               (
                 childname: value:
                   [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
@@ -34,7 +52,7 @@ let
     )
   ;
   addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
-  mkConfigFile = settings: lib.concatStringsSep "\n" (attr2Lines (addDefaults settings));
+  mkConfigFile = settings: concatStringsSep "\n" (attr2Lines (addDefaults settings));
   mkTestedConfigFile = name: settings:
     let
       configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
@@ -51,37 +69,42 @@ let
     '';
 in
 {
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
   options = {
     services.btrbk = {
-      extraPackages = lib.mkOption {
+      extraPackages = mkOption {
         description = "Extra packages for btrbk, like compression utilities for <literal>stream_compress</literal>";
-        type = lib.types.listOf lib.types.package;
+        type = types.listOf types.package;
         default = [ ];
-        example = lib.literalExpression "[ pkgs.xz ]";
+        example = literalExpression "[ pkgs.xz ]";
       };
-      niceness = lib.mkOption {
+      niceness = mkOption {
         description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
-        type = lib.types.ints.between (-20) 19;
+        type = types.ints.between (-20) 19;
         default = 10;
       };
-      ioSchedulingClass = lib.mkOption {
+      ioSchedulingClass = mkOption {
         description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
-        type = lib.types.enum [ "idle" "best-effort" "realtime" ];
+        type = types.enum [ "idle" "best-effort" "realtime" ];
         default = "best-effort";
       };
-      instances = lib.mkOption {
+      instances = mkOption {
         description = "Set of btrbk instances. The instance named <literal>btrbk</literal> is the default one.";
-        type = with lib.types;
+        type = with types;
           attrsOf (
             submodule {
               options = {
-                onCalendar = lib.mkOption {
-                  type = lib.types.str;
+                onCalendar = mkOption {
+                  type = types.nullOr types.str;
                   default = "daily";
-                  description = "How often this btrbk instance is started. See systemd.time(7) for more information about the format.";
+                  description = ''
+                    How often this btrbk instance is started. See systemd.time(7) for more information about the format.
+                    Setting it to null disables the timer, thus this instance can only be started manually.
+                  '';
                 };
-                settings = lib.mkOption {
-                  type = let t = lib.types.attrsOf (lib.types.either lib.types.str (t // { description = "instances of this type recursively"; })); in t;
+                settings = mkOption {
+                  type = let t = types.attrsOf (types.either types.str (t // { description = "instances of this type recursively"; })); in t;
                   default = { };
                   example = {
                     snapshot_preserve_min = "2d";
@@ -103,16 +126,16 @@ in
           );
         default = { };
       };
-      sshAccess = lib.mkOption {
+      sshAccess = mkOption {
         description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
-        type = with lib.types; listOf (
+        type = with types; listOf (
           submodule {
             options = {
-              key = lib.mkOption {
+              key = mkOption {
                 type = str;
                 description = "SSH public key allowed to login as user <literal>btrbk</literal> to run remote backups.";
               };
-              roles = lib.mkOption {
+              roles = mkOption {
                 type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
                 example = [ "source" "info" "send" ];
                 description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
@@ -125,7 +148,7 @@ in
     };
 
   };
-  config = lib.mkIf (sshEnabled || serviceEnabled) {
+  config = mkIf (sshEnabled || serviceEnabled) {
     environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
     security.sudo.extraRules = [
       {
@@ -152,14 +175,14 @@ in
         (
           v:
           let
-            options = lib.concatMapStringsSep " " (x: "--" + x) v.roles;
+            options = concatMapStringsSep " " (x: "--" + x) v.roles;
             ioniceClass = {
               "idle" = 3;
               "best-effort" = 2;
               "realtime" = 1;
             }.${cfg.ioSchedulingClass};
           in
-          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${lib.optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
+          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
         )
         cfg.sshAccess;
     };
@@ -169,7 +192,7 @@ in
       "d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
       "f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
     ];
-    environment.etc = lib.mapAttrs'
+    environment.etc = mapAttrs'
       (
         name: instance: {
           name = "btrbk/${name}.conf";
@@ -177,7 +200,7 @@ in
         }
       )
       cfg.instances;
-    systemd.services = lib.mapAttrs'
+    systemd.services = mapAttrs'
       (
         name: _: {
           name = "btrbk-${name}";
@@ -199,7 +222,7 @@ in
       )
       cfg.instances;
 
-    systemd.timers = lib.mapAttrs'
+    systemd.timers = mapAttrs'
       (
         name: instance: {
           name = "btrbk-${name}";
@@ -214,7 +237,8 @@ in
           };
         }
       )
-      cfg.instances;
+      (filterAttrs (name: instance: instance.onCalendar != null)
+        cfg.instances);
   };
 
 }
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 9fca21002733a..c40a0b5abc40e 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -113,9 +113,10 @@ in
         };
       };
       services.mysql-backup = {
-        description = "Mysql backup service";
+        description = "MySQL backup service";
         enable = true;
         serviceConfig = {
+          Type = "oneshot";
           User = cfg.user;
         };
         script = backupScript;
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
index 562458eb45710..f22b613382734 100644
--- a/nixos/modules/services/backup/postgresql-backup.nix
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -17,8 +17,8 @@ let
 
       compressCmd = getAttr cfg.compression {
         "none" = "cat";
-        "gzip" = "${pkgs.gzip}/bin/gzip -c";
-        "zstd" = "${pkgs.zstd}/bin/zstd -c";
+        "gzip" = "${pkgs.gzip}/bin/gzip -c -${toString cfg.compressionLevel}";
+        "zstd" = "${pkgs.zstd}/bin/zstd -c -${toString cfg.compressionLevel}";
       };
 
       mkSqlPath = prefix: suffix: "${cfg.location}/${db}${prefix}.sql${suffix}";
@@ -130,16 +130,33 @@ in {
           The type of compression to use on the generated database dump.
         '';
       };
+
+      compressionLevel = mkOption {
+        type = types.ints.between 1 19;
+        default = 6;
+        description = ''
+          The compression level used when compression is enabled.
+          gzip accepts levels 1 to 9. zstd accepts levels 1 to 19.
+        '';
+      };
     };
 
   };
 
   config = mkMerge [
     {
-      assertions = [{
-        assertion = cfg.backupAll -> cfg.databases == [];
-        message = "config.services.postgresqlBackup.backupAll cannot be used together with config.services.postgresqlBackup.databases";
-      }];
+      assertions = [
+        {
+          assertion = cfg.backupAll -> cfg.databases == [];
+          message = "config.services.postgresqlBackup.backupAll cannot be used together with config.services.postgresqlBackup.databases";
+        }
+        {
+          assertion = cfg.compression == "none" ||
+            (cfg.compression == "gzip" && cfg.compressionLevel >= 1 && cfg.compressionLevel <= 9) ||
+            (cfg.compression == "zstd" && cfg.compressionLevel >= 1 && cfg.compressionLevel <= 19);
+          message = "config.services.postgresqlBackup.compressionLevel must be set between 1 and 9 for gzip and 1 and 19 for zstd";
+        }
+      ];
     }
     (mkIf cfg.enable {
       systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 8ff8e31864be2..333fdd494e3b9 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -96,13 +96,22 @@ in
         };
 
         repository = mkOption {
-          type = types.str;
+          type = with types; nullOr str;
+          default = null;
           description = ''
             repository to backup to.
           '';
           example = "sftp:backup@192.168.1.100:/backups/${name}";
         };
 
+        repositoryFile = mkOption {
+          type = with types; nullOr path;
+          default = null;
+          description = ''
+            Path to the file containing the repository location to backup to.
+          '';
+        };
+
         paths = mkOption {
           type = types.nullOr (types.listOf types.str);
           default = null;
@@ -142,7 +151,7 @@ in
 
         extraBackupArgs = mkOption {
           type = types.listOf types.str;
-          default = [];
+          default = [ ];
           description = ''
             Extra arguments passed to restic backup.
           '';
@@ -153,7 +162,7 @@ in
 
         extraOptions = mkOption {
           type = types.listOf types.str;
-          default = [];
+          default = [ ];
           description = ''
             Extra extended options to be passed to the restic --option flag.
           '';
@@ -172,7 +181,7 @@ in
 
         pruneOpts = mkOption {
           type = types.listOf types.str;
-          default = [];
+          default = [ ];
           description = ''
             A list of options (--keep-* et al.) for 'restic forget
             --prune', to automatically prune old snapshots.  The
@@ -197,9 +206,25 @@ in
           '';
           example = "find /home/matt/git -type d -name .git";
         };
+
+        backupPrepareCommand = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            A script that must run before starting the backup process.
+          '';
+        };
+
+        backupCleanupCommand = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            A script that must run after finishing the backup process.
+          '';
+        };
       };
     }));
-    default = {};
+    default = { };
     example = {
       localbackup = {
         paths = [ "/home" ];
@@ -225,66 +250,85 @@ in
   config = {
     warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
     systemd.services =
-      mapAttrs' (name: backup:
-        let
-          extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
-          resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
-          filesFromTmpFile = "/run/restic-backups-${name}/includes";
-          backupPaths = if (backup.dynamicFilesFrom == null)
-                        then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
-                        else "--files-from ${filesFromTmpFile}";
-          pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
-            ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
-            ( resticCmd + " check" )
-          ];
-          # Helper functions for rclone remotes
-          rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
-          rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
-          rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
-          toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
-        in nameValuePair "restic-backups-${name}" ({
-          environment = {
-            RESTIC_PASSWORD_FILE = backup.passwordFile;
-            RESTIC_REPOSITORY = backup.repository;
-          } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value:
-            nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
-          ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
-            RCLONE_CONFIG = backup.rcloneConfigFile;
-          } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value:
-            nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
-          ) backup.rcloneConfig);
-          path = [ pkgs.openssh ];
-          restartIfChanged = false;
-          serviceConfig = {
-            Type = "oneshot";
-            ExecStart = (optionals (backupPaths != "") [ "${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.environmentFile != null) {
-            EnvironmentFile = backup.environmentFile;
-          };
-        } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) {
-          preStart = ''
-            ${optionalString (backup.initialize) ''
-              ${resticCmd} snapshots || ${resticCmd} init
-            ''}
-            ${optionalString (backup.dynamicFilesFrom != null) ''
-              ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
-            ''}
-          '';
-        } // optionalAttrs (backup.dynamicFilesFrom != null) {
-          postStart = ''
-            rm ${filesFromTmpFile}
-          '';
-        })
-      ) config.services.restic.backups;
+      mapAttrs'
+        (name: backup:
+          let
+            extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
+            resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
+            filesFromTmpFile = "/run/restic-backups-${name}/includes";
+            backupPaths =
+              if (backup.dynamicFilesFrom == null)
+              then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
+              else "--files-from ${filesFromTmpFile}";
+            pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
+              (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
+              (resticCmd + " check")
+            ];
+            # Helper functions for rclone remotes
+            rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
+            rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
+            rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
+            toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
+          in
+          nameValuePair "restic-backups-${name}" ({
+            environment = {
+              RESTIC_PASSWORD_FILE = backup.passwordFile;
+              RESTIC_REPOSITORY = backup.repository;
+              RESTIC_REPOSITORY_FILE = backup.repositoryFile;
+            } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs'
+              (name: value:
+                nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
+              )
+              backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
+              RCLONE_CONFIG = backup.rcloneConfigFile;
+            } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs'
+              (name: value:
+                nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
+              )
+              backup.rcloneConfig);
+            path = [ pkgs.openssh ];
+            restartIfChanged = false;
+            serviceConfig = {
+              Type = "oneshot";
+              ExecStart = (optionals (backupPaths != "") [ "${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.environmentFile != null) {
+              EnvironmentFile = backup.environmentFile;
+            };
+          } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) {
+            preStart = ''
+              ${optionalString (backup.backupPrepareCommand != null) ''
+                ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
+              ''}
+              ${optionalString (backup.initialize) ''
+                ${resticCmd} snapshots || ${resticCmd} init
+              ''}
+              ${optionalString (backup.dynamicFilesFrom != null) ''
+                ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
+              ''}
+            '';
+          } // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) {
+            postStart = ''
+              ${optionalString (backup.backupCleanupCommand != null) ''
+                ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
+              ''}
+              ${optionalString (backup.dynamicFilesFrom != null) ''
+                rm ${filesFromTmpFile}
+              ''}
+            '';
+          })
+        )
+        config.services.restic.backups;
     systemd.timers =
-      mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" {
-        wantedBy = [ "timers.target" ];
-        timerConfig = backup.timerConfig;
-      }) config.services.restic.backups;
+      mapAttrs'
+        (name: backup: nameValuePair "restic-backups-${name}" {
+          wantedBy = [ "timers.target" ];
+          timerConfig = backup.timerConfig;
+        })
+        config.services.restic.backups;
   };
 }
diff --git a/nixos/modules/services/backup/zrepl.nix b/nixos/modules/services/backup/zrepl.nix
index 4356479b6635e..1b4216ed1714a 100644
--- a/nixos/modules/services/backup/zrepl.nix
+++ b/nixos/modules/services/backup/zrepl.nix
@@ -13,6 +13,13 @@ in
     services.zrepl = {
       enable = mkEnableOption "zrepl";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.zrepl;
+        defaultText = literalExpression "pkgs.zrepl";
+        description = "Which package to use for zrepl";
+      };
+
       settings = mkOption {
         default = { };
         description = ''
@@ -30,14 +37,17 @@ in
   ### Implementation ###
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.zrepl ];
+    environment.systemPackages = [ cfg.package ];
 
     # zrepl looks for its config in this location by default. This
     # allows the use of e.g. `zrepl signal wakeup <job>` without having
     # to specify the storepath of the config.
     environment.etc."zrepl/zrepl.yml".source = configFile;
 
-    systemd.packages = [ pkgs.zrepl ];
+    systemd.packages = [ cfg.package ];
+
+    # Note that pkgs.zrepl copies and adapts the upstream systemd unit, and
+    # the fields defined here only override certain fields from that unit.
     systemd.services.zrepl = {
       requires = [ "local-fs.target" ];
       wantedBy = [ "zfs.target" ];
diff --git a/nixos/modules/services/cluster/corosync/default.nix b/nixos/modules/services/cluster/corosync/default.nix
new file mode 100644
index 0000000000000..b4144917feea9
--- /dev/null
+++ b/nixos/modules/services/cluster/corosync/default.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.corosync;
+in
+{
+  # interface
+  options.services.corosync = {
+    enable = mkEnableOption "corosync";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.corosync;
+      defaultText = literalExpression "pkgs.corosync";
+      description = "Package that should be used for corosync.";
+    };
+
+    clusterName = mkOption {
+      type = types.str;
+      default = "nixcluster";
+      description = "Name of the corosync cluster.";
+    };
+
+    extraOptions = mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = "Additional options with which to start corosync.";
+    };
+
+    nodelist = mkOption {
+      description = "Corosync nodelist: all cluster members.";
+      default = [];
+      type = with types; listOf (submodule {
+        options = {
+          nodeid = mkOption {
+            type = int;
+            description = "Node ID number";
+          };
+          name = mkOption {
+            type = str;
+            description = "Node name";
+          };
+          ring_addrs = mkOption {
+            type = listOf str;
+            description = "List of addresses, one for each ring.";
+          };
+        };
+      });
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."corosync/corosync.conf".text = ''
+      totem {
+        version: 2
+        secauth: on
+        cluster_name: ${cfg.clusterName}
+        transport: knet
+      }
+
+      nodelist {
+        ${concatMapStrings ({ nodeid, name, ring_addrs }: ''
+          node {
+            nodeid: ${toString nodeid}
+            name: ${name}
+            ${concatStrings (imap0 (i: addr: ''
+              ring${toString i}_addr: ${addr}
+            '') ring_addrs)}
+          }
+        '') cfg.nodelist}
+      }
+
+      quorum {
+        # only corosync_votequorum is supported
+        provider: corosync_votequorum
+        wait_for_all: 0
+        ${optionalString (builtins.length cfg.nodelist < 3) ''
+          two_node: 1
+        ''}
+      }
+
+      logging {
+        to_syslog: yes
+      }
+    '';
+
+    environment.etc."corosync/uidgid.d/root".text = ''
+      # allow pacemaker connection by root
+      uidgid {
+        uid: 0
+        gid: 0
+      }
+    '';
+
+    systemd.packages = [ cfg.package ];
+    systemd.services.corosync = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        StateDirectory = "corosync";
+        StateDirectoryMode = "0700";
+      };
+    };
+
+    environment.etc."sysconfig/corosync".text = lib.optionalString (cfg.extraOptions != []) ''
+      COROSYNC_OPTIONS="${lib.escapeShellArgs cfg.extraOptions}"
+    '';
+  };
+}
diff --git a/nixos/modules/services/cluster/hadoop/conf.nix b/nixos/modules/services/cluster/hadoop/conf.nix
index 0caec5cfc203f..e3c26a0d5505d 100644
--- a/nixos/modules/services/cluster/hadoop/conf.nix
+++ b/nixos/modules/services/cluster/hadoop/conf.nix
@@ -1,6 +1,6 @@
 { cfg, pkgs, lib }:
 let
-  propertyXml = name: value: ''
+  propertyXml = name: value: lib.optionalString (value != null) ''
     <property>
       <name>${name}</name>
       <value>${builtins.toString value}</value>
@@ -29,16 +29,16 @@ let
     export HADOOP_LOG_DIR=/tmp/hadoop/$USER
   '';
 in
-pkgs.runCommand "hadoop-conf" {} ''
+pkgs.runCommand "hadoop-conf" {} (with cfg; ''
   mkdir -p $out/
-  cp ${siteXml "core-site.xml" cfg.coreSite}/* $out/
-  cp ${siteXml "hdfs-site.xml" cfg.hdfsSite}/* $out/
-  cp ${siteXml "mapred-site.xml" cfg.mapredSite}/* $out/
-  cp ${siteXml "yarn-site.xml" cfg.yarnSite}/* $out/
-  cp ${siteXml "httpfs-site.xml" cfg.httpfsSite}/* $out/
-  cp ${cfgFile "container-executor.cfg" cfg.containerExecutorCfg}/* $out/
+  cp ${siteXml "core-site.xml" (coreSite // coreSiteInternal)}/* $out/
+  cp ${siteXml "hdfs-site.xml" (hdfsSiteDefault // hdfsSite // hdfsSiteInternal)}/* $out/
+  cp ${siteXml "mapred-site.xml" (mapredSiteDefault // mapredSite)}/* $out/
+  cp ${siteXml "yarn-site.xml" (yarnSiteDefault // yarnSite // yarnSiteInternal)}/* $out/
+  cp ${siteXml "httpfs-site.xml" httpfsSite}/* $out/
+  cp ${cfgFile "container-executor.cfg" containerExecutorCfg}/* $out/
   cp ${pkgs.writeTextDir "hadoop-user-functions.sh" userFunctions}/* $out/
   cp ${pkgs.writeTextDir "hadoop-env.sh" hadoopEnv}/* $out/
-  cp ${cfg.log4jProperties} $out/log4j.properties
-  ${lib.concatMapStringsSep "\n" (dir: "cp -r ${dir}/* $out/") cfg.extraConfDirs}
-''
+  cp ${log4jProperties} $out/log4j.properties
+  ${lib.concatMapStringsSep "\n" (dir: "cp -r ${dir}/* $out/") extraConfDirs}
+'')
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
index a1a95fe31cac5..a4fdea81037c7 100644
--- a/nixos/modules/services/cluster/hadoop/default.nix
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -21,24 +21,50 @@ with lib;
         <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/core-default.xml"/>
       '';
     };
+    coreSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to core-site.xml based on module options
+      '';
+    };
 
-    hdfsSite = mkOption {
+    hdfsSiteDefault = mkOption {
       default = {
         "dfs.namenode.rpc-bind-host" = "0.0.0.0";
+        "dfs.namenode.http-address" = "0.0.0.0:9870";
+        "dfs.namenode.servicerpc-bind-host" = "0.0.0.0";
+        "dfs.namenode.http-bind-host" = "0.0.0.0";
       };
       type = types.attrsOf types.anything;
+      description = ''
+        Default options for hdfs-site.xml
+      '';
+    };
+    hdfsSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
       example = literalExpression ''
         {
           "dfs.nameservices" = "namenode1";
         }
       '';
       description = ''
-        Hadoop hdfs-site.xml definition
+        Additional options and overrides for hdfs-site.xml
         <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml"/>
       '';
     };
+    hdfsSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to hdfs-site.xml based on module options
+      '';
+    };
 
-    mapredSite = mkOption {
+    mapredSiteDefault = mkOption {
       default = {
         "mapreduce.framework.name" = "yarn";
         "yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
@@ -54,18 +80,25 @@ with lib;
         }
       '';
       type = types.attrsOf types.anything;
+      description = ''
+        Default options for mapred-site.xml
+      '';
+    };
+    mapredSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
       example = literalExpression ''
-        options.services.hadoop.mapredSite.default // {
+        {
           "mapreduce.map.java.opts" = "-Xmx900m -XX:+UseParallelGC";
         }
       '';
       description = ''
-        Hadoop mapred-site.xml definition
+        Additional options and overrides for mapred-site.xml
         <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml"/>
       '';
     };
 
-    yarnSite = mkOption {
+    yarnSiteDefault = mkOption {
       default = {
         "yarn.nodemanager.admin-env" = "PATH=$PATH";
         "yarn.nodemanager.aux-services" = "mapreduce_shuffle";
@@ -77,19 +110,34 @@ with lib;
         "yarn.nodemanager.linux-container-executor.path" = "/run/wrappers/yarn-nodemanager/bin/container-executor";
         "yarn.nodemanager.log-dirs" = "/var/log/hadoop/yarn/nodemanager";
         "yarn.resourcemanager.bind-host" = "0.0.0.0";
-        "yarn.resourcemanager.scheduler.class" = "org.apache.hadoop.yarn.server.resourcemanager.scheduler.fifo.FifoScheduler";
+        "yarn.resourcemanager.scheduler.class" = "org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler";
       };
       type = types.attrsOf types.anything;
+      description = ''
+        Default options for yarn-site.xml
+      '';
+    };
+    yarnSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
       example = literalExpression ''
-        options.services.hadoop.yarnSite.default // {
+        {
           "yarn.resourcemanager.hostname" = "''${config.networking.hostName}";
         }
       '';
       description = ''
-        Hadoop yarn-site.xml definition
+        Additional options and overrides for yarn-site.xml
         <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-common/yarn-default.xml"/>
       '';
     };
+    yarnSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to yarn-site.xml based on module options
+      '';
+    };
 
     httpfsSite = mkOption {
       default = { };
@@ -123,6 +171,7 @@ with lib;
         "yarn.nodemanager.linux-container-executor.group"="hadoop";
         "min.user.id"=1000;
         "feature.terminal.enabled"=1;
+        "feature.mount-cgroup.enabled" = 1;
       };
       type = types.attrsOf types.anything;
       example = literalExpression ''
@@ -148,6 +197,8 @@ with lib;
       description = "Directories containing additional config files to be added to HADOOP_CONF_DIR";
     };
 
+    gatewayRole.enable = mkEnableOption "gateway role for deploying hadoop configs";
+
     package = mkOption {
       type = types.package;
       default = pkgs.hadoop;
@@ -157,20 +208,16 @@ with lib;
   };
 
 
-  config = mkMerge [
-    (mkIf (builtins.hasAttr "yarn" config.users.users ||
-           builtins.hasAttr "hdfs" config.users.users ||
-           builtins.hasAttr "httpfs" config.users.users) {
-      users.groups.hadoop = {
-        gid = config.ids.gids.hadoop;
-      };
-      environment = {
-        systemPackages = [ cfg.package ];
-        etc."hadoop-conf".source = let
-          hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
-        in "${hadoopConf}";
-      };
-    })
-
-  ];
+  config = mkIf cfg.gatewayRole.enable {
+    users.groups.hadoop = {
+      gid = config.ids.gids.hadoop;
+    };
+    environment = {
+      systemPackages = [ cfg.package ];
+      etc."hadoop-conf".source = let
+        hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
+      in "${hadoopConf}";
+      variables.HADOOP_CONF_DIR = "/etc/hadoop-conf/";
+    };
+  };
 }
diff --git a/nixos/modules/services/cluster/hadoop/hdfs.nix b/nixos/modules/services/cluster/hadoop/hdfs.nix
index be667aa82d8a6..325a002ad32fc 100644
--- a/nixos/modules/services/cluster/hadoop/hdfs.nix
+++ b/nixos/modules/services/cluster/hadoop/hdfs.nix
@@ -1,191 +1,191 @@
-{ config, lib, pkgs, ...}:
+{ config, lib, pkgs, ... }:
 with lib;
 let
   cfg = config.services.hadoop;
+
+  # Config files for hadoop services
   hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
-  restartIfChanged  = mkOption {
-    type = types.bool;
-    description = ''
-      Automatically restart the service on config change.
-      This can be set to false to defer restarts on clusters running critical applications.
-      Please consider the security implications of inadvertently running an older version,
-      and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
-    '';
-    default = false;
-  };
+
+  # Generator for HDFS service options
+  hadoopServiceOption = { serviceName, firewallOption ? true, extraOpts ? null }: {
+    enable = mkEnableOption serviceName;
+    restartIfChanged = mkOption {
+      type = types.bool;
+      description = ''
+        Automatically restart the service on config change.
+        This can be set to false to defer restarts on clusters running critical applications.
+        Please consider the security implications of inadvertently running an older version,
+        and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+      '';
+      default = false;
+    };
+    extraFlags = mkOption{
+      type = with types; listOf str;
+      default = [];
+      description = "Extra command line flags to pass to ${serviceName}";
+      example = [
+        "-Dcom.sun.management.jmxremote"
+        "-Dcom.sun.management.jmxremote.port=8010"
+      ];
+    };
+    extraEnv = mkOption{
+      type = with types; attrsOf str;
+      default = {};
+      description = "Extra environment variables for ${serviceName}";
+    };
+  } // (optionalAttrs firewallOption {
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open firewall ports for ${serviceName}.";
+    };
+  }) // (optionalAttrs (extraOpts != null) extraOpts);
+
+  # Generator for HDFS service configs
+  hadoopServiceConfig =
+    { name
+    , serviceOptions ? cfg.hdfs."${toLower name}"
+    , description ? "Hadoop HDFS ${name}"
+    , User ? "hdfs"
+    , allowedTCPPorts ? [ ]
+    , preStart ? ""
+    , environment ? { }
+    , extraConfig ? { }
+    }: (
+
+      mkIf serviceOptions.enable ( mkMerge [{
+        systemd.services."hdfs-${toLower name}" = {
+          inherit description preStart;
+          environment = environment // serviceOptions.extraEnv;
+          wantedBy = [ "multi-user.target" ];
+          inherit (serviceOptions) restartIfChanged;
+          serviceConfig = {
+            inherit User;
+            SyslogIdentifier = "hdfs-${toLower name}";
+            ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} ${toLower name} ${escapeShellArgs serviceOptions.extraFlags}";
+            Restart = "always";
+          };
+        };
+
+        services.hadoop.gatewayRole.enable = true;
+
+        networking.firewall.allowedTCPPorts = mkIf
+          ((builtins.hasAttr "openFirewall" serviceOptions) && serviceOptions.openFirewall)
+          allowedTCPPorts;
+      } extraConfig])
+    );
+
 in
 {
   options.services.hadoop.hdfs = {
-    namenode = {
-      enable = mkEnableOption "Whether to run the HDFS NameNode";
+
+    namenode = hadoopServiceOption { serviceName = "HDFS NameNode"; } // {
       formatOnInit = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Format HDFS namenode on first start. This is useful for quickly spinning up ephemeral HDFS clusters with a single namenode.
-          For HA clusters, initialization involves multiple steps across multiple nodes. Follow [this guide](https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html)
-          to initialize an HA cluster manually.
-        '';
-      };
-      inherit restartIfChanged;
-      openFirewall = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Open firewall ports for namenode
-        '';
-      };
-    };
-    datanode = {
-      enable = mkEnableOption "Whether to run the HDFS DataNode";
-      inherit restartIfChanged;
-      openFirewall = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Open firewall ports for datanode
+          Format HDFS namenode on first start. This is useful for quickly spinning up
+          ephemeral HDFS clusters with a single namenode.
+          For HA clusters, initialization involves multiple steps across multiple nodes.
+          Follow this guide to initialize an HA cluster manually:
+          <link xlink:href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html"/>
         '';
       };
     };
-    journalnode = {
-      enable = mkEnableOption "Whether to run the HDFS JournalNode";
-      inherit restartIfChanged;
-      openFirewall = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Open firewall ports for journalnode
-        '';
+
+    datanode = hadoopServiceOption { serviceName = "HDFS DataNode"; } // {
+      dataDirs = mkOption {
+        default = null;
+        description = "Tier and path definitions for datanode storage.";
+        type = with types; nullOr (listOf (submodule {
+          options = {
+            type = mkOption {
+              type = enum [ "SSD" "DISK" "ARCHIVE" "RAM_DISK" ];
+              description = ''
+                Storage types ([SSD]/[DISK]/[ARCHIVE]/[RAM_DISK]) for HDFS storage policies.
+              '';
+            };
+            path = mkOption {
+              type = path;
+              example = [ "/var/lib/hadoop/hdfs/dn" ];
+              description = "Determines where on the local filesystem a data node should store its blocks.";
+            };
+          };
+        }));
       };
     };
-    zkfc = {
-      enable = mkEnableOption "Whether to run the HDFS ZooKeeper failover controller";
-      inherit restartIfChanged;
+
+    journalnode = hadoopServiceOption { serviceName = "HDFS JournalNode"; };
+
+    zkfc = hadoopServiceOption {
+      serviceName = "HDFS ZooKeeper failover controller";
+      firewallOption = false;
     };
-    httpfs = {
-      enable = mkEnableOption "Whether to run the HDFS HTTPfs server";
+
+    httpfs = hadoopServiceOption { serviceName = "HDFS JournalNode"; } // {
       tempPath = mkOption {
         type = types.path;
         default = "/tmp/hadoop/httpfs";
-        description = ''
-          HTTPFS_TEMP path used by HTTPFS
-        '';
-      };
-      inherit restartIfChanged;
-      openFirewall = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Open firewall ports for HTTPFS
-        '';
+        description = "HTTPFS_TEMP path used by HTTPFS";
       };
     };
+
   };
 
   config = mkMerge [
-    (mkIf cfg.hdfs.namenode.enable {
-      systemd.services.hdfs-namenode = {
-        description = "Hadoop HDFS NameNode";
-        wantedBy = [ "multi-user.target" ];
-        inherit (cfg.hdfs.namenode) restartIfChanged;
-
-        preStart = (mkIf cfg.hdfs.namenode.formatOnInit ''
-          ${cfg.package}/bin/hdfs --config ${hadoopConf} namenode -format -nonInteractive || true
-        '');
-
-        serviceConfig = {
-          User = "hdfs";
-          SyslogIdentifier = "hdfs-namenode";
-          ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} namenode";
-          Restart = "always";
-        };
-      };
-
-      networking.firewall.allowedTCPPorts = (mkIf cfg.hdfs.namenode.openFirewall [
+    (hadoopServiceConfig {
+      name = "NameNode";
+      allowedTCPPorts = [
         9870 # namenode.http-address
         8020 # namenode.rpc-address
-        8022 # namenode. servicerpc-address
-      ]);
+        8022 # namenode.servicerpc-address
+        8019 # dfs.ha.zkfc.port
+      ];
+      preStart = (mkIf cfg.hdfs.namenode.formatOnInit
+        "${cfg.package}/bin/hdfs --config ${hadoopConf} namenode -format -nonInteractive || true"
+      );
     })
-    (mkIf cfg.hdfs.datanode.enable {
-      systemd.services.hdfs-datanode = {
-        description = "Hadoop HDFS DataNode";
-        wantedBy = [ "multi-user.target" ];
-        inherit (cfg.hdfs.datanode) restartIfChanged;
-
-        serviceConfig = {
-          User = "hdfs";
-          SyslogIdentifier = "hdfs-datanode";
-          ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} datanode";
-          Restart = "always";
-        };
-      };
 
-      networking.firewall.allowedTCPPorts = (mkIf cfg.hdfs.datanode.openFirewall [
+    (hadoopServiceConfig {
+      name = "DataNode";
+      # port numbers for datanode changed between hadoop 2 and 3
+      allowedTCPPorts = if versionAtLeast cfg.package.version "3" then [
         9864 # datanode.http.address
         9866 # datanode.address
         9867 # datanode.ipc.address
-      ]);
+      ] else [
+        50075 # datanode.http.address
+        50010 # datanode.address
+        50020 # datanode.ipc.address
+      ];
+      extraConfig.services.hadoop.hdfsSiteInternal."dfs.datanode.data.dir" = let d = cfg.hdfs.datanode.dataDirs; in
+        if (d!= null) then (concatMapStringsSep "," (x: "["+x.type+"]file://"+x.path) cfg.hdfs.datanode.dataDirs) else d;
     })
-    (mkIf cfg.hdfs.journalnode.enable {
-      systemd.services.hdfs-journalnode = {
-        description = "Hadoop HDFS JournalNode";
-        wantedBy = [ "multi-user.target" ];
-        inherit (cfg.hdfs.journalnode) restartIfChanged;
-
-        serviceConfig = {
-          User = "hdfs";
-          SyslogIdentifier = "hdfs-journalnode";
-          ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} journalnode";
-          Restart = "always";
-        };
-      };
 
-      networking.firewall.allowedTCPPorts = (mkIf cfg.hdfs.journalnode.openFirewall [
+    (hadoopServiceConfig {
+      name = "JournalNode";
+      allowedTCPPorts = [
         8480 # dfs.journalnode.http-address
         8485 # dfs.journalnode.rpc-address
-      ]);
+      ];
     })
-    (mkIf cfg.hdfs.zkfc.enable {
-      systemd.services.hdfs-zkfc = {
-        description = "Hadoop HDFS ZooKeeper failover controller";
-        wantedBy = [ "multi-user.target" ];
-        inherit (cfg.hdfs.zkfc) restartIfChanged;
-
-        serviceConfig = {
-          User = "hdfs";
-          SyslogIdentifier = "hdfs-zkfc";
-          ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} zkfc";
-          Restart = "always";
-        };
-      };
-    })
-    (mkIf cfg.hdfs.httpfs.enable {
-      systemd.services.hdfs-httpfs = {
-        description = "Hadoop httpfs";
-        wantedBy = [ "multi-user.target" ];
-        inherit (cfg.hdfs.httpfs) restartIfChanged;
-
-        environment.HTTPFS_TEMP = cfg.hdfs.httpfs.tempPath;
 
-        preStart = ''
-          mkdir -p $HTTPFS_TEMP
-        '';
+    (hadoopServiceConfig {
+      name = "zkfc";
+      description = "Hadoop HDFS ZooKeeper failover controller";
+    })
 
-        serviceConfig = {
-          User = "httpfs";
-          SyslogIdentifier = "hdfs-httpfs";
-          ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} httpfs";
-          Restart = "always";
-        };
-      };
-      networking.firewall.allowedTCPPorts = (mkIf cfg.hdfs.httpfs.openFirewall [
+    (hadoopServiceConfig {
+      name = "HTTPFS";
+      environment.HTTPFS_TEMP = cfg.hdfs.httpfs.tempPath;
+      preStart = "mkdir -p $HTTPFS_TEMP";
+      User = "httpfs";
+      allowedTCPPorts = [
         14000 # httpfs.http.port
-      ]);
+      ];
     })
-    (mkIf (
-        cfg.hdfs.namenode.enable || cfg.hdfs.datanode.enable || cfg.hdfs.journalnode.enable || cfg.hdfs.zkfc.enable
-    ) {
+
+    (mkIf cfg.gatewayRole.enable {
       users.users.hdfs = {
         description = "Hadoop HDFS user";
         group = "hadoop";
@@ -199,5 +199,6 @@ in
         isSystemUser = true;
       };
     })
+
   ];
 }
diff --git a/nixos/modules/services/cluster/hadoop/yarn.nix b/nixos/modules/services/cluster/hadoop/yarn.nix
index 37c26ea10f76f..74e16bdec687a 100644
--- a/nixos/modules/services/cluster/hadoop/yarn.nix
+++ b/nixos/modules/services/cluster/hadoop/yarn.nix
@@ -13,23 +13,77 @@ let
     '';
     default = false;
   };
+  extraFlags = mkOption{
+    type = with types; listOf str;
+    default = [];
+    description = "Extra command line flags to pass to the service";
+    example = [
+      "-Dcom.sun.management.jmxremote"
+      "-Dcom.sun.management.jmxremote.port=8010"
+    ];
+  };
+  extraEnv = mkOption{
+    type = with types; attrsOf str;
+    default = {};
+    description = "Extra environment variables";
+  };
 in
 {
   options.services.hadoop.yarn = {
     resourcemanager = {
-      enable = mkEnableOption "Whether to run the Hadoop YARN ResourceManager";
-      inherit restartIfChanged;
+      enable = mkEnableOption "Hadoop YARN ResourceManager";
+      inherit restartIfChanged extraFlags extraEnv;
+
       openFirewall = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Open firewall ports for resourcemanager
         '';
       };
     };
     nodemanager = {
-      enable = mkEnableOption "Whether to run the Hadoop YARN NodeManager";
-      inherit restartIfChanged;
+      enable = mkEnableOption "Hadoop YARN NodeManager";
+      inherit restartIfChanged extraFlags extraEnv;
+
+      resource = {
+        cpuVCores = mkOption {
+          description = "Number of vcores that can be allocated for containers.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        maximumAllocationVCores = mkOption {
+          description = "The maximum virtual CPU cores any container can be allocated.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        memoryMB = mkOption {
+          description = "Amount of physical memory, in MB, that can be allocated for containers.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        maximumAllocationMB = mkOption {
+          description = "The maximum physical memory any container can be allocated.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+      };
+
+      useCGroups = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Use cgroups to enforce resource limits on containers
+        '';
+      };
+
+      localDir = mkOption {
+        description = "List of directories to store localized files in.";
+        type = with types; nullOr (listOf path);
+        example = [ "/var/lib/hadoop/yarn/nm" ];
+        default = null;
+      };
+
       addBinBash = mkOption {
         type = types.bool;
         default = true;
@@ -39,7 +93,7 @@ in
       };
       openFirewall = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Open firewall ports for nodemanager.
           Because containers can listen on any ephemeral port, TCP ports 1024–65535 will be opened.
@@ -49,10 +103,7 @@ in
   };
 
   config = mkMerge [
-    (mkIf (
-        cfg.yarn.resourcemanager.enable || cfg.yarn.nodemanager.enable
-    ) {
-
+    (mkIf cfg.gatewayRole.enable {
       users.users.yarn = {
         description = "Hadoop YARN user";
         group = "hadoop";
@@ -65,15 +116,19 @@ in
         description = "Hadoop YARN ResourceManager";
         wantedBy = [ "multi-user.target" ];
         inherit (cfg.yarn.resourcemanager) restartIfChanged;
+        environment = cfg.yarn.resourcemanager.extraEnv;
 
         serviceConfig = {
           User = "yarn";
           SyslogIdentifier = "yarn-resourcemanager";
           ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
-                      " resourcemanager";
+                      " resourcemanager ${escapeShellArgs cfg.yarn.resourcemanager.extraFlags}";
           Restart = "always";
         };
       };
+
+      services.hadoop.gatewayRole.enable = true;
+
       networking.firewall.allowedTCPPorts = (mkIf cfg.yarn.resourcemanager.openFirewall [
         8088 # resourcemanager.webapp.address
         8030 # resourcemanager.scheduler.address
@@ -94,6 +149,7 @@ in
         description = "Hadoop YARN NodeManager";
         wantedBy = [ "multi-user.target" ];
         inherit (cfg.yarn.nodemanager) restartIfChanged;
+        environment = cfg.yarn.nodemanager.extraEnv;
 
         preStart = ''
           # create log dir
@@ -101,8 +157,9 @@ in
           chown yarn:hadoop /var/log/hadoop/yarn/nodemanager
 
           # set up setuid container executor binary
+          umount /run/wrappers/yarn-nodemanager/cgroup/cpu || true
           rm -rf /run/wrappers/yarn-nodemanager/ || true
-          mkdir -p /run/wrappers/yarn-nodemanager/{bin,etc/hadoop}
+          mkdir -p /run/wrappers/yarn-nodemanager/{bin,etc/hadoop,cgroup/cpu}
           cp ${cfg.package}/lib/${cfg.package.untarDir}/bin/container-executor /run/wrappers/yarn-nodemanager/bin/
           chgrp hadoop /run/wrappers/yarn-nodemanager/bin/container-executor
           chmod 6050 /run/wrappers/yarn-nodemanager/bin/container-executor
@@ -114,11 +171,26 @@ in
           SyslogIdentifier = "yarn-nodemanager";
           PermissionsStartOnly = true;
           ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
-                      " nodemanager";
+                      " nodemanager ${escapeShellArgs cfg.yarn.nodemanager.extraFlags}";
           Restart = "always";
         };
       };
 
+      services.hadoop.gatewayRole.enable = true;
+
+      services.hadoop.yarnSiteInternal = with cfg.yarn.nodemanager; {
+        "yarn.nodemanager.local-dirs" = localDir;
+        "yarn.scheduler.maximum-allocation-vcores" = resource.maximumAllocationVCores;
+        "yarn.scheduler.maximum-allocation-mb" = resource.maximumAllocationMB;
+        "yarn.nodemanager.resource.cpu-vcores" = resource.cpuVCores;
+        "yarn.nodemanager.resource.memory-mb" = resource.memoryMB;
+      } // mkIf useCGroups {
+        "yarn.nodemanager.linux-container-executor.cgroups.hierarchy" = "/hadoop-yarn";
+        "yarn.nodemanager.linux-container-executor.resources-handler.class" = "org.apache.hadoop.yarn.server.nodemanager.util.CgroupsLCEResourcesHandler";
+        "yarn.nodemanager.linux-container-executor.cgroups.mount" = "true";
+        "yarn.nodemanager.linux-container-executor.cgroups.mount-path" = "/run/wrappers/yarn-nodemanager/cgroup";
+      };
+
       networking.firewall.allowedTCPPortRanges = [
         (mkIf (cfg.yarn.nodemanager.openFirewall) {from = 1024; to = 65535;})
       ];
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 50b6780bbe662..bc5e8c66e2a5f 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -3,8 +3,14 @@
 with lib;
 let
   cfg = config.services.k3s;
+  removeOption = config: instruction:
+    lib.mkRemovedOptionModule ([ "services" "k3s" ] ++ config) instruction;
 in
 {
+  imports = [
+    (removeOption [ "docker" ] "k3s docker option is no longer supported.")
+  ];
+
   # interface
   options.services.k3s = {
     enable = mkEnableOption "k3s";
@@ -48,12 +54,6 @@ in
       default = null;
     };
 
-    docker = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Use docker to run containers rather than the built-in containerd.";
-    };
-
     extraFlags = mkOption {
       description = "Extra flags to pass to the k3s command.";
       type = types.str;
@@ -88,19 +88,11 @@ in
       }
     ];
 
-    virtualisation.docker = mkIf cfg.docker {
-      enable = mkDefault true;
-    };
-
-    # TODO: disable this once k3s supports cgroupsv2, either by docker
-    # supporting it, or their bundled containerd
-    systemd.enableUnifiedCgroupHierarchy = false;
-
     environment.systemPackages = [ config.services.k3s.package ];
 
     systemd.services.k3s = {
       description = "k3s service";
-      after = [ "network.service" "firewall.service" ] ++ (optional cfg.docker "docker.service");
+      after = [ "network.service" "firewall.service" ];
       wants = [ "network.service" "firewall.service" ];
       wantedBy = [ "multi-user.target" ];
       path = optional config.boot.zfs.enabled config.boot.zfs.package;
@@ -118,7 +110,7 @@ in
         ExecStart = concatStringsSep " \\\n " (
           [
             "${cfg.package}/bin/k3s ${cfg.role}"
-          ] ++ (optional cfg.docker "--docker")
+          ]
           ++ (optional cfg.disableAgent "--disable-agent")
           ++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
           ++ (optional (cfg.token != "") "--token ${cfg.token}")
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index af3a5062febc9..4363ed35d3416 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -23,7 +23,11 @@ let
   infraContainer = pkgs.dockerTools.buildImage {
     name = "pause";
     tag = "latest";
-    contents = top.package.pause;
+    copyToRoot = pkgs.buildEnv {
+      name = "image-root";
+      pathsToLink = [ "/bin" ];
+      paths = [ top.package.pause ];
+    };
     config.Cmd = ["/bin/pause"];
   };
 
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 88bde4e915576..7d9198d20e8cf 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -266,7 +266,7 @@ in
           in
           ''
             export KUBECONFIG=${clusterAdminKubeconfig}
-            ${kubectl}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files}
+            ${kubernetes}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files}
           '';
         })]);
 
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
index 2a522f1db89ce..2d95528a6ead0 100644
--- a/nixos/modules/services/cluster/kubernetes/scheduler.nix
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -66,12 +66,12 @@ in
       serviceConfig = {
         Slice = "kubernetes.slice";
         ExecStart = ''${top.package}/bin/kube-scheduler \
-          --address=${cfg.address} \
+          --bind-address=${cfg.address} \
           ${optionalString (cfg.featureGates != [])
             "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
           --kubeconfig=${top.lib.mkKubeConfig "kube-scheduler" cfg.kubeconfig} \
           --leader-elect=${boolToString cfg.leaderElect} \
-          --port=${toString cfg.port} \
+          --secure-port=${toString cfg.port} \
           ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
           ${cfg.extraOpts}
         '';
diff --git a/nixos/modules/services/cluster/pacemaker/default.nix b/nixos/modules/services/cluster/pacemaker/default.nix
new file mode 100644
index 0000000000000..7eeadffcc586b
--- /dev/null
+++ b/nixos/modules/services/cluster/pacemaker/default.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.pacemaker;
+in
+{
+  # interface
+  options.services.pacemaker = {
+    enable = mkEnableOption "pacemaker";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.pacemaker;
+      defaultText = literalExpression "pkgs.pacemaker";
+      description = "Package that should be used for pacemaker.";
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = config.services.corosync.enable;
+      message = ''
+        Enabling services.pacemaker requires a services.corosync configuration.
+      '';
+    } ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    # required by pacemaker
+    users.users.hacluster = {
+      isSystemUser = true;
+      group = "pacemaker";
+      home = "/var/lib/pacemaker";
+    };
+    users.groups.pacemaker = {};
+
+    systemd.tmpfiles.rules = [
+      "d /var/log/pacemaker 0700 hacluster pacemaker -"
+    ];
+
+    systemd.packages = [ cfg.package ];
+    systemd.services.pacemaker = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        StateDirectory = "pacemaker";
+        StateDirectoryMode = "0700";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 8cbe54c606040..b9792fd1334c3 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -361,8 +361,13 @@ in
         ++ lib.optional cfg.enableSrunX11 slurm-spank-x11;
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "systemd-tmpfiles-clean.service" ];
-      requires = [ "network.target" ];
+      after = [
+        "systemd-tmpfiles-clean.service"
+        "munge.service"
+        "network-online.target"
+        "remote-fs.target"
+      ];
+      wants = [ "network-online.target" ];
 
       serviceConfig = {
         Type = "forking";
@@ -371,6 +376,7 @@ in
         PIDFile = "/run/slurmd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         LimitMEMLOCK = "infinity";
+        Delegate="Yes";
       };
     };
 
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index aaa159d3cb18c..80c6c6abfd0b8 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -64,7 +64,7 @@ in {
         description = "Factory Steps";
         default = [];
         example = [
-          "steps.Git(repourl='git://github.com/buildbot/pyflakes.git', mode='incremental')"
+          "steps.Git(repourl='https://github.com/buildbot/pyflakes.git', mode='incremental')"
           "steps.ShellCommand(command=['trial', 'pyflakes'])"
         ];
       };
@@ -74,7 +74,7 @@ in {
         description = "List of Change Sources.";
         default = [];
         example = [
-          "changes.GitPoller('git://github.com/buildbot/pyflakes.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
+          "changes.GitPoller('https://github.com/buildbot/pyflakes.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
         ];
       };
 
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index c3bd8f99c57ff..19e73582a0904 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -34,6 +34,14 @@ in
         Repository to add the runner to.
 
         Changing this option triggers a new runner registration.
+
+        IMPORTANT: If your token is org-wide (not per repository), you need to
+        provide a github org link, not a single repository, so do it like this
+        <literal>https://github.com/nixos</literal>, not like this
+        <literal>https://github.com/nixos/nixpkgs</literal>.
+        Otherwise, you are going to get a <literal>404 NotFound</literal>
+        from <literal>POST https://api.github.com/actions/runner-registration</literal>
+        in the configure script.
       '';
       example = "https://github.com/nixos/nixpkgs";
     };
@@ -272,7 +280,6 @@ in
         CapabilityBoundingSet = "";
         # ProtectClock= adds DeviceAllow=char-rtc r
         DeviceAllow = "";
-        LockPersonality = true;
         NoNewPrivileges = true;
         PrivateDevices = true;
         PrivateMounts = true;
@@ -291,11 +298,36 @@ in
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
         UMask = "0066";
+        ProtectProc = "invisible";
+        SystemCallFilter = [
+          "~@clock"
+          "~@cpu-emulation"
+          "~@module"
+          "~@mount"
+          "~@obsolete"
+          "~@raw-io"
+          "~@reboot"
+          "~capset"
+          "~setdomainname"
+          "~sethostname"
+        ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
 
         # Needs network access
         PrivateNetwork = false;
         # Cannot be true due to Node
         MemoryDenyWriteExecute = false;
+
+        # The more restrictive "pid" option makes `nix` commands in CI emit
+        # "GC Warning: Couldn't read /proc/stat"
+        # You may want to set this to "pid" if not using `nix` commands
+        ProcSubset = "all";
+        # Coverage programs for compiled code such as `cargo-tarpaulin` disable
+        # ASLR (address space layout randomization) which requires the
+        # `personality` syscall
+        # You may want to set this to `true` if not using coverage tooling on
+        # compiled code
+        LockPersonality = false;
       };
     };
   };
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index dc58c63452392..85ac0fb2a8907 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -36,12 +36,12 @@ let
 
       # register new services
       ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
-        if echo "$NEW_SERVICES" | grep -xq ${name}; then
+        if echo "$NEW_SERVICES" | grep -xq "${name}"; then
           bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
             "set -a && source ${service.registrationConfigFile} &&"
             "gitlab-runner register"
             "--non-interactive"
-            "--name ${name}"
+            (if service.description != null then "--description \"${service.description}\"" else "--name '${name}'")
             "--executor ${service.executor}"
             "--limit ${toString service.limit}"
             "--request-concurrency ${toString service.requestConcurrency}"
@@ -365,6 +365,13 @@ in
               with <literal>RUNNER_ENV</literal> variable set.
             '';
           };
+          description = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = ''
+              Name/description of the runner.
+            '';
+          };
           executor = mkOption {
             type = types.str;
             default = "docker";
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index cc5de97d6d10d..d3945f2370d71 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -99,8 +99,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.hydra-unstable;
-        defaultText = literalExpression "pkgs.hydra-unstable";
+        default = pkgs.hydra_unstable;
+        defaultText = literalExpression "pkgs.hydra_unstable";
         description = "The Hydra package.";
       };
 
@@ -298,27 +298,32 @@ in
         environment = env // {
           HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
         };
+        path = [ pkgs.util-linux ];
         preStart = ''
           mkdir -p ${baseDir}
-          chown hydra.hydra ${baseDir}
+          chown hydra:hydra ${baseDir}
           chmod 0750 ${baseDir}
 
           ln -sf ${hydraConf} ${baseDir}/hydra.conf
 
           mkdir -m 0700 -p ${baseDir}/www
-          chown hydra-www.hydra ${baseDir}/www
+          chown hydra-www:hydra ${baseDir}/www
 
           mkdir -m 0700 -p ${baseDir}/queue-runner
           mkdir -m 0750 -p ${baseDir}/build-logs
-          chown hydra-queue-runner.hydra ${baseDir}/queue-runner ${baseDir}/build-logs
+          mkdir -m 0750 -p ${baseDir}/runcommand-logs
+          chown hydra-queue-runner.hydra \
+            ${baseDir}/queue-runner \
+            ${baseDir}/build-logs \
+            ${baseDir}/runcommand-logs
 
           ${optionalString haveLocalDB ''
             if ! [ -e ${baseDir}/.db-created ]; then
-              ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
-              ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra
+              runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
+              runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra
               touch ${baseDir}/.db-created
             fi
-            echo "create extension if not exists pg_trgm" | ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
+            echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
           ''}
 
           if [ ! -e ${cfg.gcRootsDir} ]; then
@@ -338,7 +343,7 @@ in
             rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
           fi
 
-          chown hydra.hydra ${cfg.gcRootsDir}
+          chown hydra:hydra ${cfg.gcRootsDir}
           chmod 2775 ${cfg.gcRootsDir}
         '';
         serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 3ca1542c18f2a..49b39b03d4758 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -156,12 +156,22 @@ in {
           reloadScript = ''
             echo "Asking Jenkins to reload config"
             curl_opts="--silent --fail --show-error"
-            access_token=${if cfg.accessTokenFile != ""
-                           then "$(cat '${cfg.accessTokenFile}')"
-                           else cfg.accessToken}
-            jenkins_url="http://${cfg.accessUser}:$access_token@${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
-            crumb=$(curl $curl_opts "$jenkins_url"'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)')
-            curl $curl_opts -X POST -H "$crumb" "$jenkins_url"/reload
+            access_token_file=${if cfg.accessTokenFile != ""
+                           then cfg.accessTokenFile
+                           else "$RUNTIME_DIRECTORY/jenkins_access_token.txt"}
+            if [ "${cfg.accessToken}" != "" ]; then
+               (umask 0077; printf "${cfg.accessToken}" >"$access_token_file")
+            fi
+            jenkins_url="http://${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
+            auth_file="$RUNTIME_DIRECTORY/jenkins_auth_file.txt"
+            trap 'rm -f "$auth_file"' EXIT
+            (umask 0077; printf "${cfg.accessUser}:@password_placeholder@" >"$auth_file")
+            "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "$access_token_file" "$auth_file"
+
+            if ! "${pkgs.jenkins}/bin/jenkins-cli" -s "$jenkins_url" -auth "@$auth_file" reload-configuration; then
+                echo "error: failed to reload configuration"
+                exit 1
+            fi
           '';
         in
           ''
@@ -233,6 +243,7 @@ in {
             done
           '' + (if cfg.accessUser != "" then reloadScript else "");
       serviceConfig = {
+        Type = "oneshot";
         User = jenkinsCfg.user;
         RuntimeDirectory = "jenkins-job-builder";
       };
diff --git a/nixos/modules/services/continuous-integration/jenkins/slave.nix b/nixos/modules/services/continuous-integration/jenkins/slave.nix
index 3c0e6f78e74ca..871b9914fb27a 100644
--- a/nixos/modules/services/continuous-integration/jenkins/slave.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/slave.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 with lib;
 let
   cfg = config.services.jenkinsSlave;
@@ -46,6 +46,15 @@ in {
           this is the home of the "jenkins" user.
         '';
       };
+
+      javaPackage = mkOption {
+        default = pkgs.jdk;
+        defaultText = literalExpression "pkgs.jdk";
+        description = ''
+          Java package to install.
+        '';
+        type = types.package;
+      };
     };
   };
 
@@ -64,5 +73,10 @@ in {
         uid = config.ids.uids.jenkins;
       };
     };
+
+    programs.java = {
+      enable = true;
+      package = cfg.javaPackage;
+    };
   };
 }
diff --git a/nixos/modules/services/databases/cassandra.nix b/nixos/modules/services/databases/cassandra.nix
index b36cac35e7c29..b457e69babaad 100644
--- a/nixos/modules/services/databases/cassandra.nix
+++ b/nixos/modules/services/databases/cassandra.nix
@@ -4,11 +4,12 @@ let
   inherit (lib)
     concatStringsSep
     flip
-    literalDocBook
+    literalMD
     literalExpression
     optionalAttrs
     optionals
     recursiveUpdate
+    mdDoc
     mkEnableOption
     mkIf
     mkOption
@@ -107,7 +108,7 @@ in
     clusterName = mkOption {
       type = types.str;
       default = "Test Cluster";
-      description = ''
+      description = mdDoc ''
         The name of the cluster.
         This setting prevents nodes in one logical cluster from joining
         another. All nodes in a cluster must have the same value.
@@ -117,19 +118,19 @@ in
     user = mkOption {
       type = types.str;
       default = defaultUser;
-      description = "Run Apache Cassandra under this user.";
+      description = mdDoc "Run Apache Cassandra under this user.";
     };
 
     group = mkOption {
       type = types.str;
       default = defaultUser;
-      description = "Run Apache Cassandra under this group.";
+      description = mdDoc "Run Apache Cassandra under this group.";
     };
 
     homeDir = mkOption {
       type = types.path;
       default = "/var/lib/cassandra";
-      description = ''
+      description = mdDoc ''
         Home directory for Apache Cassandra.
       '';
     };
@@ -139,7 +140,7 @@ in
       default = pkgs.cassandra;
       defaultText = literalExpression "pkgs.cassandra";
       example = literalExpression "pkgs.cassandra_3_11";
-      description = ''
+      description = mdDoc ''
         The Apache Cassandra package to use.
       '';
     };
@@ -147,8 +148,8 @@ in
     jvmOpts = mkOption {
       type = types.listOf types.str;
       default = [ ];
-      description = ''
-        Populate the JVM_OPT environment variable.
+      description = mdDoc ''
+        Populate the `JVM_OPT` environment variable.
       '';
     };
 
@@ -156,20 +157,20 @@ in
       type = types.nullOr types.str;
       default = "127.0.0.1";
       example = null;
-      description = ''
+      description = mdDoc ''
         Address or interface to bind to and tell other Cassandra nodes
         to connect to. You _must_ change this if you want multiple
         nodes to be able to communicate!
 
-        Set listenAddress OR listenInterface, not both.
+        Set {option}`listenAddress` OR {option}`listenInterface`, not both.
 
         Leaving it blank leaves it up to
-        InetAddress.getLocalHost(). This will always do the Right
-        Thing _if_ the node is properly configured (hostname, name
+        `InetAddress.getLocalHost()`. This will always do the "Right
+        Thing" _if_ the node is properly configured (hostname, name
         resolution, etc), and the Right Thing is to use the address
         associated with the hostname (it might not be).
 
-        Setting listen_address to 0.0.0.0 is always wrong.
+        Setting {option}`listenAddress` to `0.0.0.0` is always wrong.
       '';
     };
 
@@ -177,8 +178,8 @@ in
       type = types.nullOr types.str;
       default = null;
       example = "eth1";
-      description = ''
-        Set listenAddress OR listenInterface, not both. Interfaces
+      description = mdDoc ''
+        Set `listenAddress` OR `listenInterface`, not both. Interfaces
         must correspond to a single address, IP aliasing is not
         supported.
       '';
@@ -188,18 +189,18 @@ in
       type = types.nullOr types.str;
       default = "127.0.0.1";
       example = null;
-      description = ''
+      description = mdDoc ''
         The address or interface to bind the native transport server to.
 
-        Set rpcAddress OR rpcInterface, not both.
+        Set {option}`rpcAddress` OR {option}`rpcInterface`, not both.
 
-        Leaving rpcAddress blank has the same effect as on
-        listenAddress (i.e. it will be based on the configured hostname
+        Leaving {option}`rpcAddress` blank has the same effect as on
+        {option}`listenAddress` (i.e. it will be based on the configured hostname
         of the node).
 
-        Note that unlike listenAddress, you can specify 0.0.0.0, but you
-        must also set extraConfig.broadcast_rpc_address to a value other
-        than 0.0.0.0.
+        Note that unlike {option}`listenAddress`, you can specify `"0.0.0.0"`, but you
+        must also set `extraConfig.broadcast_rpc_address` to a value other
+        than `"0.0.0.0"`.
 
         For security reasons, you should not expose this port to the
         internet. Firewall it if needed.
@@ -210,8 +211,8 @@ in
       type = types.nullOr types.str;
       default = null;
       example = "eth1";
-      description = ''
-        Set rpcAddress OR rpcInterface, not both. Interfaces must
+      description = mdDoc ''
+        Set {option}`rpcAddress` OR {option}`rpcInterface`, not both. Interfaces must
         correspond to a single address, IP aliasing is not supported.
       '';
     };
@@ -233,7 +234,7 @@ in
           <logger name="com.thinkaurelius.thrift" level="ERROR"/>
         </configuration>
       '';
-      description = ''
+      description = mdDoc ''
         XML logback configuration for cassandra
       '';
     };
@@ -241,24 +242,24 @@ in
     seedAddresses = mkOption {
       type = types.listOf types.str;
       default = [ "127.0.0.1" ];
-      description = ''
+      description = mdDoc ''
         The addresses of hosts designated as contact points in the cluster. A
         joining node contacts one of the nodes in the seeds list to learn the
         topology of the ring.
-        Set to 127.0.0.1 for a single node cluster.
+        Set to `[ "127.0.0.1" ]` for a single node cluster.
       '';
     };
 
     allowClients = mkOption {
       type = types.bool;
       default = true;
-      description = ''
+      description = mdDoc ''
         Enables or disables the native transport server (CQL binary protocol).
-        This server uses the same address as the <literal>rpcAddress</literal>,
-        but the port it uses is not <literal>rpc_port</literal> but
-        <literal>native_transport_port</literal>. See the official Cassandra
+        This server uses the same address as the {option}`rpcAddress`,
+        but the port it uses is not `rpc_port` but
+        `native_transport_port`. See the official Cassandra
         docs for more information on these variables and set them using
-        <literal>extraConfig</literal>.
+        {option}`extraConfig`.
       '';
     };
 
@@ -269,8 +270,8 @@ in
         {
           commitlog_sync_batch_window_in_ms = 3;
         };
-      description = ''
-        Extra options to be merged into cassandra.yaml as nix attribute set.
+      description = mdDoc ''
+        Extra options to be merged into {file}`cassandra.yaml` as nix attribute set.
       '';
     };
 
@@ -278,8 +279,8 @@ in
       type = types.lines;
       default = "";
       example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"'';
-      description = ''
-        Extra shell lines to be appended onto cassandra-env.sh.
+      description = mdDoc ''
+        Extra shell lines to be appended onto {file}`cassandra-env.sh`.
       '';
     };
 
@@ -287,13 +288,13 @@ in
       type = types.nullOr types.str;
       default = "3w";
       example = null;
-      description = ''
+      description = mdDoc ''
         Set the interval how often full repairs are run, i.e.
-        <literal>nodetool repair --full</literal> is executed. See
-        https://cassandra.apache.org/doc/latest/operating/repair.html
+        {command}`nodetool repair --full` is executed. See
+        <https://cassandra.apache.org/doc/latest/operating/repair.html>
         for more information.
 
-        Set to <literal>null</literal> to disable full repairs.
+        Set to `null` to disable full repairs.
       '';
     };
 
@@ -301,7 +302,7 @@ in
       type = types.listOf types.str;
       default = [ ];
       example = [ "--partitioner-range" ];
-      description = ''
+      description = mdDoc ''
         Options passed through to the full repair command.
       '';
     };
@@ -310,13 +311,13 @@ in
       type = types.nullOr types.str;
       default = "3d";
       example = null;
-      description = ''
+      description = mdDoc ''
         Set the interval how often incremental repairs are run, i.e.
-        <literal>nodetool repair</literal> is executed. See
-        https://cassandra.apache.org/doc/latest/operating/repair.html
+        {command}`nodetool repair` is executed. See
+        <https://cassandra.apache.org/doc/latest/operating/repair.html>
         for more information.
 
-        Set to <literal>null</literal> to disable incremental repairs.
+        Set to `null` to disable incremental repairs.
       '';
     };
 
@@ -324,7 +325,7 @@ in
       type = types.listOf types.str;
       default = [ ];
       example = [ "--partitioner-range" ];
-      description = ''
+      description = mdDoc ''
         Options passed through to the incremental repair command.
       '';
     };
@@ -333,15 +334,15 @@ in
       type = types.nullOr types.str;
       default = null;
       example = "4G";
-      description = ''
-        Must be left blank or set together with heapNewSize.
+      description = mdDoc ''
+        Must be left blank or set together with {option}`heapNewSize`.
         If left blank a sensible value for the available amount of RAM and CPU
         cores is calculated.
 
         Override to set the amount of memory to allocate to the JVM at
         start-up. For production use you may wish to adjust this for your
-        environment. MAX_HEAP_SIZE is the total amount of memory dedicated
-        to the Java heap. HEAP_NEWSIZE refers to the size of the young
+        environment. `MAX_HEAP_SIZE` is the total amount of memory dedicated
+        to the Java heap. `HEAP_NEWSIZE` refers to the size of the young
         generation.
 
         The main trade-off for the young generation is that the larger it
@@ -354,21 +355,21 @@ in
       type = types.nullOr types.str;
       default = null;
       example = "800M";
-      description = ''
-        Must be left blank or set together with heapNewSize.
+      description = mdDoc ''
+        Must be left blank or set together with {option}`heapNewSize`.
         If left blank a sensible value for the available amount of RAM and CPU
         cores is calculated.
 
         Override to set the amount of memory to allocate to the JVM at
         start-up. For production use you may wish to adjust this for your
-        environment. HEAP_NEWSIZE refers to the size of the young
+        environment. `HEAP_NEWSIZE` refers to the size of the young
         generation.
 
         The main trade-off for the young generation is that the larger it
         is, the longer GC pause times will be. The shorter it is, the more
         expensive GC will be (usually).
 
-        The example HEAP_NEWSIZE assumes a modern 8-core+ machine for decent pause
+        The example `HEAP_NEWSIZE` assumes a modern 8-core+ machine for decent pause
         times. If in doubt, and if you do not particularly want to tweak, go with
         100 MB per physical CPU core.
       '';
@@ -378,7 +379,7 @@ in
       type = types.nullOr types.int;
       default = null;
       example = 4;
-      description = ''
+      description = mdDoc ''
         Set this to control the amount of arenas per-thread in glibc.
       '';
     };
@@ -386,19 +387,19 @@ in
     remoteJmx = mkOption {
       type = types.bool;
       default = false;
-      description = ''
+      description = mdDoc ''
         Cassandra ships with JMX accessible *only* from localhost.
         To enable remote JMX connections set to true.
 
         Be sure to also enable authentication and/or TLS.
-        See: https://wiki.apache.org/cassandra/JmxSecurity
+        See: <https://wiki.apache.org/cassandra/JmxSecurity>
       '';
     };
 
     jmxPort = mkOption {
       type = types.int;
       default = 7199;
-      description = ''
+      description = mdDoc ''
         Specifies the default port over which Cassandra will be available for
         JMX connections.
         For security reasons, you should not expose this port to the internet.
@@ -408,11 +409,11 @@ in
 
     jmxRoles = mkOption {
       default = [ ];
-      description = ''
-        Roles that are allowed to access the JMX (e.g. nodetool)
-        BEWARE: The passwords will be stored world readable in the nix-store.
+      description = mdDoc ''
+        Roles that are allowed to access the JMX (e.g. {command}`nodetool`)
+        BEWARE: The passwords will be stored world readable in the nix store.
                 It's recommended to use your own protected file using
-                <literal>jmxRolesFile</literal>
+                {option}`jmxRolesFile`
 
         Doesn't work in versions older than 3.11 because they don't like that
         it's world readable.
@@ -437,7 +438,7 @@ in
         if versionAtLeast cfg.package.version "3.11"
         then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
         else null;
-      defaultText = literalDocBook ''generated configuration file if version is at least 3.11, otherwise <literal>null</literal>'';
+      defaultText = literalMD ''generated configuration file if version is at least 3.11, otherwise `null`'';
       example = "/var/lib/cassandra/jmx.password";
       description = ''
         Specify your own jmx roles file.
diff --git a/nixos/modules/services/databases/cockroachdb.nix b/nixos/modules/services/databases/cockroachdb.nix
index eb061af926219..9a7aebe4f6ae9 100644
--- a/nixos/modules/services/databases/cockroachdb.nix
+++ b/nixos/modules/services/databases/cockroachdb.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -6,46 +6,44 @@ let
   cfg = config.services.cockroachdb;
   crdb = cfg.package;
 
-  escape    = builtins.replaceStrings ["%"] ["%%"];
-  ifNotNull = v: s: optionalString (v != null) s;
-
-  startupCommand = lib.concatStringsSep " "
-    [ # Basic startup
-      "${crdb}/bin/cockroach start"
+  startupCommand = utils.escapeSystemdExecArgs
+    ([
+      # Basic startup
+      "${crdb}/bin/cockroach"
+      "start"
       "--logtostderr"
       "--store=/var/lib/cockroachdb"
-      (ifNotNull cfg.locality "--locality='${cfg.locality}'")
 
       # WebUI settings
-      "--http-addr='${cfg.http.address}:${toString cfg.http.port}'"
+      "--http-addr=${cfg.http.address}:${toString cfg.http.port}"
 
       # Cluster listen address
-      "--listen-addr='${cfg.listen.address}:${toString cfg.listen.port}'"
-
-      # Cluster configuration
-      (ifNotNull cfg.join "--join=${cfg.join}")
+      "--listen-addr=${cfg.listen.address}:${toString cfg.listen.port}"
 
-      # Cache and memory settings. Must be escaped.
-      "--cache='${escape cfg.cache}'"
-      "--max-sql-memory='${escape cfg.maxSqlMemory}'"
+      # Cache and memory settings.
+      "--cache=${cfg.cache}"
+      "--max-sql-memory=${cfg.maxSqlMemory}"
 
       # Certificate/security settings.
       (if cfg.insecure then "--insecure" else "--certs-dir=${cfg.certsDir}")
-    ];
-
-    addressOption = descr: defaultPort: {
-      address = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = "Address to bind to for ${descr}";
-      };
+    ]
+    ++ lib.optional (cfg.join != null) "--join=${cfg.join}"
+    ++ lib.optional (cfg.locality != null) "--locality=${cfg.locality}"
+    ++ cfg.extraArgs);
+
+  addressOption = descr: defaultPort: {
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Address to bind to for ${descr}";
+    };
 
-      port = mkOption {
-        type = types.port;
-        default = defaultPort;
-        description = "Port to bind to for ${descr}";
-      };
+    port = mkOption {
+      type = types.port;
+      default = defaultPort;
+      description = "Port to bind to for ${descr}";
     };
+  };
 in
 
 {
@@ -159,6 +157,16 @@ in
           only contain open source features and open source code).
         '';
       };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--advertise-addr" "[fe80::f6f2:::]" ];
+        description = ''
+          Extra CLI arguments passed to <command>cockroach start</command>.
+          For the full list of supported argumemnts, check <link xlink:href="https://www.cockroachlabs.com/docs/stable/cockroach-start.html#flags"/>
+        '';
+      };
     };
   };
 
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index 742e605d224db..39d1ead28fc0c 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -193,6 +193,11 @@ in {
 
       preStart = ''
         touch ${cfg.configFile}
+        if ! test -e ${cfg.databaseDir}/.erlang.cookie; then
+          touch ${cfg.databaseDir}/.erlang.cookie
+          chmod 600 ${cfg.databaseDir}/.erlang.cookie
+          dd if=/dev/random bs=16 count=1 | base64 > ${cfg.databaseDir}/.erlang.cookie
+        fi
       '';
 
       environment = {
@@ -204,6 +209,7 @@ in {
         ERL_FLAGS= ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'';
         # 5. the vm.args file
         COUCHDB_ARGS_FILE=''${cfg.argsFile}'';
+        HOME =''${cfg.databaseDir}'';
       };
 
       serviceConfig = {
diff --git a/nixos/modules/services/databases/dgraph.nix b/nixos/modules/services/databases/dgraph.nix
new file mode 100644
index 0000000000000..5c1ae536051c1
--- /dev/null
+++ b/nixos/modules/services/databases/dgraph.nix
@@ -0,0 +1,148 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dgraph;
+  settingsFormat = pkgs.formats.json {};
+  configFile = settingsFormat.generate "config.json" cfg.settings;
+  dgraphWithNode = pkgs.runCommand "dgraph" {
+    nativeBuildInputs = [ pkgs.makeWrapper ];
+  }
+  ''
+    mkdir -p $out/bin
+    makeWrapper ${cfg.package}/bin/dgraph $out/bin/dgraph \
+      --set PATH '${lib.makeBinPath [ pkgs.nodejs ]}:$PATH' \
+  '';
+  securityOptions = {
+      NoNewPrivileges = true;
+
+      AmbientCapabilities = "";
+      CapabilityBoundingSet = "";
+
+      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;
+      RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+
+      SystemCallArchitectures = "native";
+      SystemCallErrorNumber = "EPERM";
+      SystemCallFilter = [
+        "@system-service"
+        "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+      ];
+  };
+in
+{
+  options = {
+    services.dgraph = {
+      enable = mkEnableOption "Dgraph native GraphQL database with a graph backend";
+
+      package = lib.mkPackageOption pkgs "dgraph" { };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = {};
+        description = ''
+          Contents of the dgraph config. For more details see https://dgraph.io/docs/deploy/config
+        '';
+      };
+
+      alpha = {
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            The host which dgraph alpha will be run on.
+          '';
+        };
+        port = mkOption {
+          type = types.port;
+          default = 7080;
+          description = ''
+            The port which to run dgraph alpha on.
+          '';
+        };
+
+      };
+
+      zero = {
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            The host which dgraph zero will be run on.
+          '';
+        };
+        port = mkOption {
+          type = types.port;
+          default = 5080;
+          description = ''
+            The port which to run dgraph zero on.
+          '';
+        };
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.dgraph.settings = {
+      badger.compression = mkDefault "zstd:3";
+    };
+
+    systemd.services.dgraph-zero = {
+      description = "Dgraph native GraphQL database with a graph backend. Zero controls node clustering";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        StateDirectory = "dgraph-zero";
+        WorkingDirectory = "/var/lib/dgraph-zero";
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/dgraph zero --my ${cfg.zero.host}:${toString cfg.zero.port}";
+        Restart = "on-failure";
+      } // securityOptions;
+    };
+
+    systemd.services.dgraph-alpha = {
+      description = "Dgraph native GraphQL database with a graph backend. Alpha serves data";
+      after = [ "network.target" "dgraph-zero.service" ];
+      requires = [ "dgraph-zero.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        StateDirectory = "dgraph-alpha";
+        WorkingDirectory = "/var/lib/dgraph-alpha";
+        DynamicUser = true;
+        ExecStart = "${dgraphWithNode}/bin/dgraph alpha --config ${configFile} --my ${cfg.alpha.host}:${toString cfg.alpha.port} --zero ${cfg.zero.host}:${toString cfg.zero.port}";
+        ExecStop = ''
+          ${pkgs.curl}/bin/curl --data "mutation { shutdown { response { message code } } }" \
+              --header 'Content-Type: application/graphql' \
+              -X POST \
+              http://localhost:8080/admin
+        '';
+        Restart = "on-failure";
+      } // securityOptions;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ happysalada ];
+}
diff --git a/nixos/modules/services/databases/dragonflydb.nix b/nixos/modules/services/databases/dragonflydb.nix
new file mode 100644
index 0000000000000..e72afa9d90890
--- /dev/null
+++ b/nixos/modules/services/databases/dragonflydb.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dragonflydb;
+  dragonflydb = pkgs.dragonflydb;
+
+  settings =
+    {
+      port = cfg.port;
+      dir = "/var/lib/dragonflydb";
+      keys_output_limit = cfg.keysOutputLimit;
+    } //
+    (lib.optionalAttrs (cfg.bind != null) { bind = cfg.bind; }) //
+    (lib.optionalAttrs (cfg.requirePass != null) { requirepass = cfg.requirePass; }) //
+    (lib.optionalAttrs (cfg.maxMemory != null) { maxmemory = cfg.maxMemory; }) //
+    (lib.optionalAttrs (cfg.memcachePort != null) { memcache_port = cfg.memcachePort; }) //
+    (lib.optionalAttrs (cfg.dbNum != null) { dbnum = cfg.dbNum; }) //
+    (lib.optionalAttrs (cfg.cacheMode != null) { cache_mode = cfg.cacheMode; });
+in
+{
+
+  ###### interface
+
+  options = {
+    services.dragonflydb = {
+      enable = mkEnableOption "DragonflyDB";
+
+      user = mkOption {
+        type = types.str;
+        default = "dragonfly";
+        description = "The user to run DragonflyDB as";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 6379;
+        description = "The TCP port to accept connections.";
+      };
+
+      bind = mkOption {
+        type = with types; nullOr str;
+        default = "127.0.0.1";
+        description = ''
+          The IP interface to bind to.
+          <literal>null</literal> means "all interfaces".
+        '';
+      };
+
+      requirePass = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "Password for database";
+        example = "letmein!";
+      };
+
+      maxMemory = mkOption {
+        type = with types; nullOr ints.unsigned;
+        default = null;
+        description = ''
+          The maximum amount of memory to use for storage (in bytes).
+          <literal>null</literal> means this will be automatically set.
+        '';
+      };
+
+      memcachePort = mkOption {
+        type = with types; nullOr port;
+        default = null;
+        description = ''
+          To enable memcached compatible API on this port.
+          <literal>null</literal> means disabled.
+        '';
+      };
+
+      keysOutputLimit = mkOption {
+        type = types.ints.unsigned;
+        default = 8192;
+        description = ''
+          Maximum number of returned keys in keys command.
+          <literal>keys</literal> is a dangerous command.
+          We truncate its result to avoid blowup in memory when fetching too many keys.
+        '';
+      };
+
+      dbNum = mkOption {
+        type = with types; nullOr ints.unsigned;
+        default = null;
+        description = "Maximum number of supported databases for <literal>select</literal>";
+      };
+
+      cacheMode = mkOption {
+        type = with types; nullOr bool;
+        default = null;
+        description = ''
+          Once this mode is on, Dragonfly will evict items least likely to be stumbled
+          upon in the future but only when it is near maxmemory limit.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.dragonflydb.enable {
+
+    users.users = optionalAttrs (cfg.user == "dragonfly") {
+      dragonfly.description = "DragonflyDB server user";
+      dragonfly.isSystemUser = true;
+      dragonfly.group = "dragonfly";
+    };
+    users.groups = optionalAttrs (cfg.user == "dragonfly") { dragonfly = { }; };
+
+    environment.systemPackages = [ dragonflydb ];
+
+    systemd.services.dragonflydb = {
+      description = "DragonflyDB server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${dragonflydb}/bin/dragonfly --alsologtostderr ${builtins.concatStringsSep " " (attrsets.mapAttrsToList (n: v: "--${n} ${strings.escapeShellArg v}") settings)}";
+
+        User = cfg.user;
+
+        # Filesystem access
+        ReadWritePaths = [ settings.dir ];
+        StateDirectory = "dragonflydb";
+        StateDirectoryMode = "0700";
+        # Process Properties
+        LimitMEMLOCK = "infinity";
+        # Caps
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        LockPersonality = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        MemoryDenyWriteExecute = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 2c1e25d430840..7c7340094e2c5 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -245,7 +245,7 @@ in {
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
+  meta.maintainers = with lib.maintainers; [ kwohlfahrt ];
 
   config = mkIf cfg.enable {
     assertions = map (opt: {
@@ -268,9 +268,14 @@ in {
     };
 
     systemd.services.openldap = {
-      description = "LDAP server";
+      description = "OpenLDAP Server Daemon";
+      documentation = [
+        "man:slapd"
+        "man:slapd-config"
+        "man:slapd-mdb"
+      ];
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
+      after = [ "network-online.target" ];
       preStart = let
         settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
 
@@ -306,7 +311,8 @@ in {
           "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
           "-h" (lib.concatStringsSep " " cfg.urlList)
         ]);
-        Type = "forking";
+        Type = "notify";
+        NotifyAccess = "all";
         PIDFile = cfg.settings.attrs.olcPidFile;
       };
     };
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 2919022496a36..550bd36efff28 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -295,7 +295,8 @@ in
       # Note: when changing the default, make it conditional on
       # ‘system.stateVersion’ to maintain compatibility with existing
       # systems!
-      mkDefault (if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
+      mkDefault (if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
+            else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
             else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
             else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
             else mkThrow "9_5");
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index e0269a962fdd8..5532c54019706 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -81,7 +81,9 @@ in {
             user = mkOption {
               type = types.str;
               default = redisName name;
-              defaultText = "\"redis\" or \"redis-\${name}\" if name != \"\"";
+              defaultText = literalExpression ''
+                if name == "" then "redis" else "redis-''${name}"
+              '';
               description = "The username and groupname for redis-server.";
             };
 
@@ -105,8 +107,7 @@ in {
 
             bind = mkOption {
               type = with types; nullOr str;
-              default = if name == "" then "127.0.0.1" else null;
-              defaultText = literalExpression ''if name == "" then "127.0.0.1" else null'';
+              default = "127.0.0.1";
               description = ''
                 The IP interface to bind to.
                 <literal>null</literal> means "all interfaces".
@@ -117,7 +118,9 @@ in {
             unixSocket = mkOption {
               type = with types; nullOr path;
               default = "/run/${redisName name}/redis.sock";
-              defaultText = "\"/run/redis/redis.sock\" or \"/run/redis-\${name}/redis.sock\" if name != \"\"";
+              defaultText = literalExpression ''
+                if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock"
+              '';
               description = "The path to the socket to bind to.";
             };
 
@@ -163,7 +166,11 @@ in {
             save = mkOption {
               type = with types; listOf (listOf int);
               default = [ [900 1] [300 10] [60 10000] ];
-              description = "The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.";
+              description = mdDoc ''
+                The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.
+
+                If set to the empty list (`[]`) then RDB persistence will be disabled (useful if you are using AOF or don't want any persistence).
+              '';
             };
 
             slaveOf = mkOption {
@@ -265,7 +272,11 @@ in {
               syslog-enabled = config.syslog;
               databases = config.databases;
               maxclients = config.maxclients;
-              save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
+              save = if config.save == []
+                then ''""'' # Disable saving with `save = ""`
+                else map
+                  (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}")
+                  config.save;
               dbfilename = "dump.rdb";
               dir = "/var/lib/${redisName name}";
               appendOnly = config.appendOnly;
@@ -370,7 +381,7 @@ in {
         ProtectKernelTunables = true;
         ProtectControlGroups = true;
         RestrictAddressFamilies =
-          optionals (conf.bind != null) ["AF_INET" "AF_INET6"] ++
+          optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++
           optional (conf.unixSocket != null) "AF_UNIX";
         RestrictNamespaces = true;
         LockPersonality = true;
diff --git a/nixos/modules/services/databases/riak.nix b/nixos/modules/services/databases/riak.nix
deleted file mode 100644
index cc4237d038cdb..0000000000000
--- a/nixos/modules/services/databases/riak.nix
+++ /dev/null
@@ -1,162 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.riak;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.riak = {
-
-      enable = mkEnableOption "riak";
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.riak;
-        defaultText = literalExpression "pkgs.riak";
-        description = ''
-          Riak package to use.
-        '';
-      };
-
-      nodeName = mkOption {
-        type = types.str;
-        default = "riak@127.0.0.1";
-        description = ''
-          Name of the Erlang node.
-        '';
-      };
-
-      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";
-        description = ''
-          Data directory for Riak.
-        '';
-      };
-
-      logDir = mkOption {
-        type = types.path;
-        default = "/var/log/riak";
-        description = ''
-          Log directory for Riak.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>riak.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/riak.conf".text = ''
-      nodename = ${cfg.nodeName}
-      distributed_cookie = ${cfg.distributedCookie}
-
-      platform_log_dir = ${cfg.logDir}
-      platform_etc_dir = /etc/riak
-      platform_data_dir = ${cfg.dataDir}
-
-      ${cfg.extraConfig}
-    '';
-
-    environment.etc."riak/advanced.config".text = ''
-      ${cfg.extraAdvancedConfig}
-    '';
-
-    users.users.riak = {
-      name = "riak";
-      uid = config.ids.uids.riak;
-      group = "riak";
-      description = "Riak server user";
-    };
-
-    users.groups.riak.gid = config.ids.gids.riak;
-
-    systemd.services.riak = {
-      description = "Riak Server";
-
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      path = [
-        pkgs.util-linux # for `logger`
-        pkgs.bash
-      ];
-
-      environment.HOME = "${cfg.dataDir}";
-      environment.RIAK_DATA_DIR = "${cfg.dataDir}";
-      environment.RIAK_LOG_DIR = "${cfg.logDir}";
-      environment.RIAK_ETC_DIR = "/etc/riak";
-
-      preStart = ''
-        if ! test -e ${cfg.logDir}; then
-          mkdir -m 0755 -p ${cfg.logDir}
-          chown -R riak ${cfg.logDir}
-        fi
-
-        if ! test -e ${cfg.dataDir}; then
-          mkdir -m 0700 -p ${cfg.dataDir}
-          chown -R riak ${cfg.dataDir}
-        fi
-      '';
-
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/riak console";
-        ExecStop = "${cfg.package}/bin/riak stop";
-        StandardInput = "tty";
-        User = "riak";
-        Group = "riak";
-        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/virtuoso.nix b/nixos/modules/services/databases/virtuoso.nix
deleted file mode 100644
index 8b01622ecb039..0000000000000
--- a/nixos/modules/services/databases/virtuoso.nix
+++ /dev/null
@@ -1,99 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  cfg = config.services.virtuoso;
-  virtuosoUser = "virtuoso";
-  stateDir = "/var/lib/virtuoso";
-in
-with lib;
-{
-
-  ###### interface
-
-  options = {
-
-    services.virtuoso = {
-
-      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";
-      };
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    users.users.${virtuosoUser} =
-      { uid = config.ids.uids.virtuoso;
-        description = "virtuoso user";
-        home = stateDir;
-      };
-
-    systemd.services.virtuoso = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart = ''
-        mkdir -p ${stateDir}
-        chown ${virtuosoUser} ${stateDir}
-      '';
-
-      script = ''
-        cd ${stateDir}
-        ${pkgs.virtuoso}/bin/virtuoso-t +foreground +configfile ${pkgs.writeText "virtuoso.ini" cfg.config}
-      '';
-    };
-
-    services.virtuoso.config = ''
-      [Database]
-      DatabaseFile=${stateDir}/x-virtuoso.db
-      TransactionFile=${stateDir}/x-virtuoso.trx
-      ErrorLogFile=${stateDir}/x-virtuoso.log
-      xa_persistent_file=${stateDir}/x-virtuoso.pxa
-
-      [Parameters]
-      ServerPort=${cfg.listenAddress}
-      RunAs=${virtuosoUser}
-      ${optionalString (cfg.dirsAllowed != null) "DirsAllowed=${cfg.dirsAllowed}"}
-      ${cfg.parameters}
-
-      [HTTPServer]
-      ${optionalString (cfg.httpListenAddress != null) "ServerPort=${cfg.httpListenAddress}"}
-    '';
-
-  };
-
-}
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
index 7da92cc9f2649..5fecc64b4f70b 100644
--- a/nixos/modules/services/desktops/flatpak.nix
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -30,6 +30,8 @@ in {
 
     environment.systemPackages = [ pkgs.flatpak ];
 
+    security.polkit.enable = true;
+
     services.dbus.packages = [ pkgs.flatpak ];
 
     systemd.packages = [ pkgs.flatpak ];
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
index 284d8c394a611..9aa51b61431db 100644
--- a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
+++ b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
@@ -8,7 +8,7 @@
   },
   "context.modules": [
     {
-      "name": "libpipewire-module-rtkit",
+      "name": "libpipewire-module-rt",
       "args": {},
       "flags": [
         "ifexists",
diff --git a/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json b/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
new file mode 100644
index 0000000000000..0f1ebe5749c67
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
@@ -0,0 +1,120 @@
+{
+  "context.properties": {
+    "link.max-buffers": 16,
+    "core.daemon": true,
+    "core.name": "pipewire-0",
+    "settings.check-quantum": true,
+    "settings.check-rate": true,
+    "vm.overrides": {
+      "default.clock.min-quantum": 1024
+    }
+  },
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "api.alsa.*": "alsa/libspa-alsa",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rt",
+      "args": {
+        "nice.level": -11
+      },
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-profiler"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-spa-node-factory"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-access",
+      "args": {}
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-link-factory"
+    }
+  ],
+  "context.objects": [
+    {
+      "factory": "metadata",
+      "args": {
+        "metadata.name": "default"
+      }
+    },
+    {
+      "factory": "spa-node-factory",
+      "args": {
+        "factory.name": "support.node.driver",
+        "node.name": "Dummy-Driver",
+        "node.group": "pipewire.dummy",
+        "priority.driver": 20000
+      }
+    },
+    {
+      "factory": "spa-node-factory",
+      "args": {
+        "factory.name": "support.node.driver",
+        "node.name": "Freewheel-Driver",
+        "priority.driver": 19000,
+        "node.group": "pipewire.freewheel",
+        "node.freewheel": true
+      }
+    },
+    {
+      "factory": "adapter",
+      "args": {
+        "factory.name": "api.alsa.pcm.source",
+        "node.name": "system",
+        "node.description": "system",
+        "media.class": "Audio/Source",
+        "api.alsa.path": "hw:0",
+        "node.suspend-on-idle": true,
+        "resample.disable": true,
+        "channelmix.disable": true,
+        "adapter.auto-port-config": {
+          "mode": "dsp",
+          "monitor": false,
+          "control": false,
+          "position": "unknown"
+        }
+      }
+    },
+    {
+      "factory": "adapter",
+      "args": {
+        "factory.name": "api.alsa.pcm.sink",
+        "node.name": "system",
+        "node.description": "system",
+        "media.class": "Audio/Sink",
+        "api.alsa.path": "hw:0",
+        "node.suspend-on-idle": true,
+        "resample.disable": true,
+        "channelmix.disable": true,
+        "adapter.auto-port-config": {
+          "mode": "dsp",
+          "monitor": false,
+          "control": false,
+          "position": "unknown"
+        }
+      }
+    }
+  ],
+  "context.exec": []
+}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
index 3ed994f111458..114afbfb0ea46 100644
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
+++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
@@ -6,8 +6,10 @@
   },
   "context.modules": [
     {
-      "name": "libpipewire-module-rtkit",
-      "args": {},
+      "name": "libpipewire-module-rt",
+      "args": {
+        "nice.level": -11
+      },
       "flags": [
         "ifexists",
         "nofail"
@@ -27,16 +29,75 @@
     },
     {
       "name": "libpipewire-module-protocol-pulse",
-      "args": {
-        "server.address": [
-          "unix:native"
-        ],
-        "vm.overrides": {
+      "args": {}
+    }
+  ],
+  "context.exec": [
+    {
+      "path": "pactl",
+      "args": "load-module module-always-sink"
+    }
+  ],
+  "stream.properties": {},
+  "pulse.properties": {
+    "server.address": [
+      "unix:native"
+    ],
+    "vm.overrides": {
+      "pulse.min.quantum": "1024/48000"
+    }
+  },
+  "pulse.rules": [
+    {
+      "matches": [
+        {}
+      ],
+      "actions": {
+        "update-props": {}
+      }
+    },
+    {
+      "matches": [
+        {
+          "application.process.binary": "teams"
+        },
+        {
+          "application.process.binary": "teams-insiders"
+        },
+        {
+          "application.process.binary": "skypeforlinux"
+        }
+      ],
+      "actions": {
+        "quirks": [
+          "force-s16-info"
+        ]
+      }
+    },
+    {
+      "matches": [
+        {
+          "application.process.binary": "firefox"
+        }
+      ],
+      "actions": {
+        "quirks": [
+          "remove-capture-dont-move"
+        ]
+      }
+    },
+    {
+      "matches": [
+        {
+          "application.name": "~speech-dispatcher*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "pulse.min.req": "1024/48000",
           "pulse.min.quantum": "1024/48000"
         }
       }
     }
-  ],
-  "context.exec": [],
-  "stream.properties": {}
+  ]
 }
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
index a923ab4db2357..7c79f0168c022 100644
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
+++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
@@ -3,6 +3,7 @@
     "link.max-buffers": 16,
     "core.daemon": true,
     "core.name": "pipewire-0",
+    "default.clock.min-quantum": 16,
     "vm.overrides": {
       "default.clock.min-quantum": 1024
     }
@@ -19,8 +20,10 @@
   },
   "context.modules": [
     {
-      "name": "libpipewire-module-rtkit",
-      "args": {},
+      "name": "libpipewire-module-rt",
+      "args": {
+        "nice.level": -11
+      },
       "flags": [
         "ifexists",
         "nofail"
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
index f7a03a4a3eaf1..09761d6300e83 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
@@ -38,9 +38,8 @@ in {
     services.pipewire.media-session = {
       enable = mkOption {
         type = types.bool;
-        default = config.services.pipewire.enable;
-        defaultText = literalExpression "config.services.pipewire.enable";
-        description = "Example pipewire session manager";
+        default = false;
+        description = "Whether to enable the deprecated example Pipewire session manager";
       };
 
       package = mkOption {
@@ -59,7 +58,7 @@ in {
             Configuration for the media session core. For details see
             https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/media-session.conf
           '';
-          default = {};
+          default = defaults.media-session;
         };
 
         alsa-monitor = mkOption {
@@ -68,7 +67,7 @@ in {
             Configuration for the alsa monitor. For details see
             https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/alsa-monitor.conf
           '';
-          default = {};
+          default = defaults.alsa-monitor;
         };
 
         bluez-monitor = mkOption {
@@ -77,7 +76,7 @@ in {
             Configuration for the bluez5 monitor. For details see
             https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/bluez-monitor.conf
           '';
-          default = {};
+          default = defaults.bluez-monitor;
         };
 
         v4l2-monitor = mkOption {
@@ -86,7 +85,7 @@ in {
             Configuration for the V4L2 monitor. For details see
             https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/v4l2-monitor.conf
           '';
-          default = {};
+          default = defaults.v4l2-monitor;
         };
       };
     };
@@ -111,6 +110,11 @@ in {
       source = json.generate "v4l2-monitor.conf" configs.v4l2-monitor;
     };
 
+    environment.etc."pipewire/media-session.d/with-audio" =
+      mkIf config.services.pipewire.audio.enable {
+        text = "";
+      };
+
     environment.etc."pipewire/media-session.d/with-alsa" =
       mkIf config.services.pipewire.alsa.enable {
         text = "";
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
index c3cfd46e61c21..dd1f5e3a018db 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -25,15 +25,18 @@ let
     client = lib.importJSON ./daemon/client.conf.json;
     client-rt = lib.importJSON ./daemon/client-rt.conf.json;
     jack = lib.importJSON ./daemon/jack.conf.json;
+    minimal = lib.importJSON ./daemon/minimal.conf.json;
     pipewire = lib.importJSON ./daemon/pipewire.conf.json;
     pipewire-pulse = lib.importJSON ./daemon/pipewire-pulse.conf.json;
   };
 
+  useSessionManager = cfg.wireplumber.enable || cfg.media-session.enable;
+
   configs = {
     client = recursiveUpdate defaults.client cfg.config.client;
     client-rt = recursiveUpdate defaults.client-rt cfg.config.client-rt;
     jack = recursiveUpdate defaults.jack cfg.config.jack;
-    pipewire = recursiveUpdate defaults.pipewire cfg.config.pipewire;
+    pipewire = recursiveUpdate (if useSessionManager then defaults.pipewire else defaults.minimal) cfg.config.pipewire;
     pipewire-pulse = recursiveUpdate defaults.pipewire-pulse cfg.config.pipewire-pulse;
   };
 in {
@@ -113,6 +116,16 @@ in {
         };
       };
 
+      audio = {
+        enable = lib.mkOption {
+          type = lib.types.bool;
+          # this is for backwards compatibility
+          default = cfg.alsa.enable || cfg.jack.enable || cfg.pulse.enable;
+          defaultText = lib.literalExpression "config.services.pipewire.alsa.enable || config.services.pipewire.jack.enable || config.services.pipewire.pulse.enable";
+          description = "Whether to use PipeWire as the primary sound server";
+        };
+      };
+
       alsa = {
         enable = mkEnableOption "ALSA support";
         support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems";
@@ -149,13 +162,18 @@ in {
   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.audio.enable -> !config.hardware.pulseaudio.enable;
+        message = "Using PipeWire as the sound server conflicts with 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";
       }
+      {
+        # JACK intentionally not checked, as PW-on-JACK setups are a thing that some people may want
+        assertion = (cfg.alsa.enable || cfg.pulse.enable) -> cfg.audio.enable;
+        message = "Using PipeWire's ALSA/PulseAudio compatibility layers requires running PipeWire as the sound server. Set `services.pipewire.audio.enable` to true.";
+      }
     ];
 
     environment.systemPackages = [ cfg.package ]
@@ -216,12 +234,12 @@ in {
     environment.etc."pipewire/pipewire.conf" = {
       source = json.generate "pipewire.conf" configs.pipewire;
     };
-    environment.etc."pipewire/pipewire-pulse.conf" = {
+    environment.etc."pipewire/pipewire-pulse.conf" = mkIf cfg.pulse.enable {
       source = json.generate "pipewire-pulse.conf" configs.pipewire-pulse;
     };
 
     environment.sessionVariables.LD_LIBRARY_PATH =
-      lib.optional cfg.jack.enable "${cfg.package.jack}/lib";
+      lib.mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ];
 
     users = lib.mkIf cfg.systemWide {
       users.pipewire = {
@@ -233,6 +251,8 @@ in {
         ] ++ lib.optional config.security.rtkit.enable "rtkit";
         description = "Pipewire system service user";
         isSystemUser = true;
+        home = "/var/lib/pipewire";
+        createHome = true;
       };
       groups.pipewire.gid = config.ids.gids.pipewire;
     };
@@ -240,5 +260,8 @@ in {
     # https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/464#note_723554
     systemd.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
     systemd.user.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
+
+    # pipewire-pulse default config expects pactl to be in PATH
+    systemd.user.services.pipewire-pulse.path = lib.mkIf cfg.pulse.enable [ pkgs.pulseaudio ];
   };
 }
diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix
index ad96dc1f9745e..004392c7873c4 100644
--- a/nixos/modules/services/desktops/pipewire/wireplumber.nix
+++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix
@@ -1,22 +1,27 @@
 { config, lib, pkgs, ... }:
 
 let
-  cfg = config.services.pipewire.wireplumber;
+  pwCfg = config.services.pipewire;
+  cfg = pwCfg.wireplumber;
+  pwUsedForAudio = pwCfg.audio.enable;
 in
 {
   meta.maintainers = [ lib.maintainers.k900 ];
 
   options = {
     services.pipewire.wireplumber = {
-      enable = lib.mkEnableOption "A modular session / policy manager for PipeWire";
+      enable = lib.mkOption {
+        type = lib.types.bool;
+        default = config.services.pipewire.enable;
+        defaultText = lib.literalExpression "config.services.pipewire.enable";
+        description = "Whether to enable Wireplumber, a modular session / policy manager for PipeWire";
+      };
 
       package = lib.mkOption {
         type = lib.types.package;
         default = pkgs.wireplumber;
         defaultText = lib.literalExpression "pkgs.wireplumber";
-        description = ''
-          The wireplumber derivation to use.
-        '';
+        description = "The wireplumber derivation to use.";
       };
     };
   };
@@ -30,6 +35,28 @@ in
     ];
 
     environment.systemPackages = [ cfg.package ];
+
+    environment.etc."wireplumber/main.lua.d/80-nixos.lua" = lib.mkIf (!pwUsedForAudio) {
+      text = ''
+        -- Pipewire is not used for audio, so prevent it from grabbing audio devices
+        alsa_monitor.enable = function() end
+      '';
+    };
+    environment.etc."wireplumber/main.lua.d/80-systemwide.lua" = lib.mkIf config.services.pipewire.systemWide {
+      text = ''
+        -- When running system-wide, these settings need to be disabled (they
+        -- use functions that aren't available on the system dbus).
+        alsa_monitor.properties["alsa.reserve"] = false
+        default_access.properties["enable-flatpak-portal"] = false
+      '';
+    };
+    environment.etc."wireplumber/bluetooth.lua.d/80-systemwide.lua" = lib.mkIf config.services.pipewire.systemWide {
+      text = ''
+        -- When running system-wide, logind-integration needs to be disabled.
+        bluez_monitor.properties["with-logind"] = false
+      '';
+    };
+
     systemd.packages = [ cfg.package ];
 
     systemd.services.wireplumber.enable = config.services.pipewire.systemWide;
@@ -37,5 +64,10 @@ in
 
     systemd.services.wireplumber.wantedBy = [ "pipewire.service" ];
     systemd.user.services.wireplumber.wantedBy = [ "pipewire.service" ];
+
+    systemd.services.wireplumber.environment = lib.mkIf config.services.pipewire.systemWide {
+      # Force wireplumber to use system dbus.
+      DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/dbus/system_bus_socket";
+    };
   };
 }
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
index bebb3c3f13f01..7c86e8b647854 100644
--- a/nixos/modules/services/development/jupyter/default.nix
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -143,6 +143,9 @@ in {
             language = "python";
             logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
             logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
+            extraPaths = {
+              "cool.txt" = pkgs.writeText "cool" "cool content";
+            };
           };
         }
       '';
@@ -194,6 +197,7 @@ in {
         extraGroups = [ cfg.group ];
         home = "/var/lib/jupyter";
         createHome = true;
+        isSystemUser = true;
         useDefaultShell = true; # needed so that the user can start a terminal.
       };
     })
diff --git a/nixos/modules/services/development/jupyter/kernel-options.nix b/nixos/modules/services/development/jupyter/kernel-options.nix
index 348a8b44b382b..0a9eaafa31856 100644
--- a/nixos/modules/services/development/jupyter/kernel-options.nix
+++ b/nixos/modules/services/development/jupyter/kernel-options.nix
@@ -56,5 +56,14 @@ with lib;
         Path to 64x64 logo png.
       '';
     };
+
+    extraPaths = mkOption {
+      type = types.attrsOf types.path;
+      default = { };
+      example = literalExpression ''"{ examples = ''${env.sitePack}/IRkernel/kernelspec/kernel.js"; }'';
+      description = ''
+        Extra paths to link in kernel directory
+      '';
+    };
   };
 }
diff --git a/nixos/modules/services/development/zammad.nix b/nixos/modules/services/development/zammad.nix
new file mode 100644
index 0000000000000..d457a60718730
--- /dev/null
+++ b/nixos/modules/services/development/zammad.nix
@@ -0,0 +1,323 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zammad;
+  settingsFormat = pkgs.formats.yaml { };
+  filterNull = filterAttrs (_: v: v != null);
+  serviceConfig = {
+    Type = "simple";
+    Restart = "always";
+
+    User = "zammad";
+    Group = "zammad";
+    PrivateTmp = true;
+    StateDirectory = "zammad";
+    WorkingDirectory = cfg.dataDir;
+  };
+  environment = {
+    RAILS_ENV = "production";
+    NODE_ENV = "production";
+    RAILS_SERVE_STATIC_FILES = "true";
+    RAILS_LOG_TO_STDOUT = "true";
+  };
+  databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings;
+in
+{
+
+  options = {
+    services.zammad = {
+      enable = mkEnableOption "Zammad, a web-based, open source user support/ticketing solution.";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.zammad;
+        defaultText = literalExpression "pkgs.zammad";
+        description = "Zammad package to use.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/zammad";
+        description = ''
+          Path to a folder that will contain Zammad working directory.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "192.168.23.42";
+        description = "Host address.";
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open firewall ports for Zammad";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = "Web service port.";
+      };
+
+      websocketPort = mkOption {
+        type = types.port;
+        default = 6042;
+        description = "Websocket service port.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "PostgreSQL" "MySQL" ];
+          default = "PostgreSQL";
+          example = "MySQL";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.nullOr types.str;
+          default = {
+            PostgreSQL = "/run/postgresql";
+            MySQL = "localhost";
+          }.${cfg.database.type};
+          defaultText = literalExpression ''
+            {
+              PostgreSQL = "/run/postgresql";
+              MySQL = "localhost";
+            }.''${config.services.zammad.database.type};
+          '';
+          description = ''
+            Database host address.
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.port;
+          default = null;
+          description = "Database port. Use <literal>null</literal> for default port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zammad";
+          description = ''
+            Database name.
+          '';
+        };
+
+        user = mkOption {
+          type = types.nullOr types.str;
+          default = "zammad";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/zammad-dbpassword";
+          description = ''
+            A file containing the password for <option>services.zammad.database.user</option>.
+          '';
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+
+        settings = mkOption {
+          type = settingsFormat.type;
+          default = { };
+          example = literalExpression ''
+            {
+            }
+          '';
+          description = ''
+            The <filename>database.yml</filename> configuration file as key value set.
+            See <link xlink:href='TODO' />
+            for list of configuration parameters.
+          '';
+        };
+      };
+
+      secretKeyBaseFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/secret_key_base";
+        description = ''
+          The path to a file containing the
+          <literal>secret_key_base</literal> secret.
+
+          Zammad uses <literal>secret_key_base</literal> to encrypt
+          the cookie store, which contains session data, and to digest
+          user auth tokens.
+
+          Needs to be a 64 byte long string of hexadecimal
+          characters. You can generate one by running
+
+          <screen>
+          <prompt>$ </prompt>openssl rand -hex 64 >/path/to/secret_key_base_file
+          </screen>
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.zammad.database.settings = {
+      production = mapAttrs (_: v: mkDefault v) (filterNull {
+        adapter = {
+          PostgreSQL = "postgresql";
+          MySQL = "mysql2";
+        }.${cfg.database.type};
+        database = cfg.database.name;
+        pool = 50;
+        timeout = 5000;
+        encoding = "utf8";
+        username = cfg.database.user;
+        host = cfg.database.host;
+        port = cfg.database.port;
+      });
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openPorts [
+      config.services.zammad.port
+      config.services.zammad.websocketPort
+    ];
+
+    users.users.zammad = {
+      isSystemUser = true;
+      home = cfg.dataDir;
+      group = "zammad";
+    };
+
+    users.groups.zammad = { };
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.database.user == "zammad";
+        message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true";
+      }
+      {
+        assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.zammad.database.createLocally is set to true";
+      }
+    ];
+
+    services.mysql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "MySQL") {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "PostgreSQL") {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd.services.zammad-web = {
+      inherit environment;
+      serviceConfig = serviceConfig // {
+        # loading all the gems takes time
+        TimeoutStartSec = 1200;
+      };
+      after = [
+        "network.target"
+        "postgresql.service"
+      ];
+      requires = [
+        "postgresql.service"
+      ];
+      description = "Zammad web";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        # Blindly copy the whole project here.
+        chmod -R +w .
+        rm -rf ./public/assets/*
+        rm -rf ./tmp/*
+        rm -rf ./log/*
+        cp -r --no-preserve=owner ${cfg.package}/* .
+        chmod -R +w .
+        # config file
+        cp ${databaseConfig} ./config/database.yml
+        chmod -R +w .
+        ${optionalString (cfg.database.passwordFile != null) ''
+        {
+          echo -n "  password: "
+          cat ${cfg.database.passwordFile}
+        } >> ./config/database.yml
+        ''}
+        ${optionalString (cfg.secretKeyBaseFile != null) ''
+        {
+          echo "production: "
+          echo -n "  secret_key_base: "
+          cat ${cfg.secretKeyBaseFile}
+        } > ./config/secrets.yml
+        ''}
+
+        if [ `${config.services.postgresql.package}/bin/psql \
+                  --host ${cfg.database.host} \
+                  ${optionalString
+                    (cfg.database.port != null)
+                    "--port ${toString cfg.database.port}"} \
+                  --username ${cfg.database.user} \
+                  --dbname ${cfg.database.name} \
+                  --command "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
+          echo "Initialize database"
+          ./bin/rake --no-system db:migrate
+          ./bin/rake --no-system db:seed
+        else
+          echo "Migrate database"
+          ./bin/rake --no-system db:migrate
+        fi
+        echo "Done"
+      '';
+      script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}";
+    };
+
+    systemd.services.zammad-websocket = {
+      inherit serviceConfig environment;
+      after = [ "zammad-web.service" ];
+      requires = [ "zammad-web.service" ];
+      description = "Zammad websocket";
+      wantedBy = [ "multi-user.target" ];
+      script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start";
+    };
+
+    systemd.services.zammad-scheduler = {
+      inherit environment;
+      serviceConfig = serviceConfig // { Type = "forking"; };
+      after = [ "zammad-web.service" ];
+      requires = [ "zammad-web.service" ];
+      description = "Zammad scheduler";
+      wantedBy = [ "multi-user.target" ];
+      script = "./script/scheduler.rb start";
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ garbas taeer ];
+}
diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
index 895961707d363..4fc6ca3674915 100644
--- a/nixos/modules/services/display-managers/greetd.nix
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -54,7 +54,7 @@ in
   config = mkIf cfg.enable {
 
     services.greetd.settings.terminal.vt = mkDefault cfg.vt;
-    services.greetd.settings.default_session = mkDefault "greeter";
+    services.greetd.settings.default_session.user = mkDefault "greeter";
 
     security.pam.services.greetd = {
       allowNullPassword = true;
diff --git a/nixos/modules/services/editors/haste.nix b/nixos/modules/services/editors/haste.nix
new file mode 100644
index 0000000000000..35fe26766ef7d
--- /dev/null
+++ b/nixos/modules/services/editors/haste.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  pkg = pkgs.haste-server;
+  cfg = config.services.haste-server;
+
+  format = pkgs.formats.json {};
+in
+{
+  options.services.haste-server = {
+    enable = mkEnableOption "haste-server";
+    openFirewall = mkEnableOption "firewall passthrough for haste-server";
+
+    settings = mkOption {
+      description = ''
+        Configuration for haste-server.
+        For documentation see <link xlink:href="https://github.com/toptal/haste-server#settings">project readme</link>
+      '';
+      type = format.type;
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.settings.port ];
+
+    services.haste-server = {
+      settings = {
+        host = mkDefault "::";
+        port = mkDefault 7777;
+
+        keyLength = mkDefault 10;
+        maxLength = mkDefault 400000;
+
+        staticMaxAge = mkDefault 86400;
+        recompressStaticAssets = mkDefault false;
+
+        logging = mkDefault [
+          {
+            level = "verbose";
+            type = "Console";
+            colorize = true;
+          }
+        ];
+
+        keyGenerator = mkDefault {
+          type = "phonetic";
+        };
+
+        rateLimits = {
+          categories = {
+            normal = {
+              totalRequests = mkDefault 500;
+              every = mkDefault 60000;
+            };
+          };
+        };
+
+        storage = mkDefault {
+          type = "file";
+        };
+
+        documents = {
+          about = mkDefault "${pkg}/share/haste-server/about.md";
+        };
+      };
+    };
+
+    systemd.services.haste-server = {
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "network.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = "haste-server";
+        DynamicUser = true;
+        StateDirectory = "haste-server";
+        WorkingDirectory = "/var/lib/haste-server";
+        ExecStart = "${pkg}/bin/haste-server ${format.generate "config.json" cfg.settings}";
+      };
+
+      path = with pkgs; [ pkg coreutils ];
+    };
+  };
+}
diff --git a/nixos/modules/services/games/asf.nix b/nixos/modules/services/games/asf.nix
index ea2bfd40fffaa..ed1a5544d7a48 100644
--- a/nixos/modules/services/games/asf.nix
+++ b/nixos/modules/services/games/asf.nix
@@ -13,6 +13,8 @@ let
     # is in theory not needed as this is already the default for default builds
     UpdateChannel = 0;
     Headless = true;
+  } // lib.optionalAttrs (cfg.ipcPasswordFile != null) {
+    IPCPassword = "#ipcPassword#";
   });
 
   ipc-config = format.generate "IPC.config" cfg.ipcSettings;
@@ -81,8 +83,7 @@ in
       type = format.type;
       description = ''
         The ASF.json file, all the options are documented <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#global-config">here</link>.
-        Do note that `AutoRestart`  and `UpdateChannel` is always to `false`
-respectively `0` because NixOS takes care of updating everything.
+        Do note that `AutoRestart`  and `UpdateChannel` is always to `false` respectively `0` because NixOS takes care of updating everything.
         `Headless` is also always set to `true` because there is no way to provide inputs via a systemd service.
         You should try to keep ASF up to date since upstream does not provide support for anything but the latest version and you're exposing yourself to all kinds of issues - as is outlined <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#updateperiod">here</link>.
       '';
@@ -92,6 +93,12 @@ respectively `0` because NixOS takes care of updating everything.
       default = { };
     };
 
+    ipcPasswordFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "Path to a file containig the password. The file must be readable by the <literal>asf</literal> user/group.";
+    };
+
     ipcSettings = mkOption {
       type = format.type;
       description = ''
@@ -115,14 +122,12 @@ respectively `0` because NixOS takes care of updating everything.
         options = {
           username = mkOption {
             type = types.str;
-            description =
-              "Name of the user to log in. Default is attribute name.";
+            description = "Name of the user to log in. Default is attribute name.";
             default = "";
           };
           passwordFile = mkOption {
             type = types.path;
-            description =
-              "Path to a file containig the password. The file must be readable by the <literal>asf</literal> user/group.";
+            description = "Path to a file containig the password. The file must be readable by the <literal>asf</literal> user/group.";
           };
           enabled = mkOption {
             type = types.bool;
@@ -131,8 +136,7 @@ respectively `0` because NixOS takes care of updating everything.
           };
           settings = mkOption {
             type = types.attrs;
-            description =
-              "Additional settings that are documented <link xlink:href=\"https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#bot-config\">here</link>.";
+            description = "Additional settings that are documented <link xlink:href=\"https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#bot-config\">here</link>.";
             default = { };
           };
         };
@@ -170,14 +174,17 @@ respectively `0` because NixOS takes care of updating everything.
         wantedBy = [ "multi-user.target" ];
 
         serviceConfig = mkMerge [
-          (mkIf (cfg.dataDir == "/var/lib/asf") { StateDirectory = "asf"; })
+          (mkIf (cfg.dataDir == "/var/lib/asf") {
+            StateDirectory = "asf";
+            StateDirectoryMode = "700";
+          })
           {
             User = "asf";
             Group = "asf";
             WorkingDirectory = cfg.dataDir;
             Type = "simple";
-            ExecStart =
-              "${cfg.package}/bin/ArchiSteamFarm --path ${cfg.dataDir} --process-required --no-restart --service --no-config-migrate";
+            ExecStart = "${cfg.package}/bin/ArchiSteamFarm --path ${cfg.dataDir} --process-required --no-restart --service --no-config-migrate";
+            Restart = "always";
 
             # mostly copied from the default systemd service
             PrivateTmp = true;
@@ -202,35 +209,47 @@ respectively `0` because NixOS takes care of updating everything.
           }
         ];
 
-        preStart = ''
-          mkdir -p config
-          rm -f www
-          rm -f config/{*.json,*.config}
-
-          ln -s ${asf-config} config/ASF.json
-
-          ${strings.optionalString (cfg.ipcSettings != {}) ''
-            ln -s ${ipc-config} config/IPC.config
-          ''}
-
-          ln -s ${pkgs.runCommandLocal "ASF-bots" {} ''
-            mkdir -p $out/lib/asf/bots
-            for i in ${strings.concatStringsSep " " (lists.map (x: "${getName x},${x}") (attrsets.mapAttrsToList mkBot cfg.bots))}; do IFS=",";
-              set -- $i
-              ln -s $2 $out/lib/asf/bots/$1
-            done
-          ''}/lib/asf/bots/* config/
-
-          ${strings.optionalString cfg.web-ui.enable ''
-            ln -s ${cfg.web-ui.package}/lib/dist www
-          ''}
-        '';
+        preStart =
+          let
+            createBotsScript = pkgs.runCommandLocal "ASF-bots" { } ''
+              mkdir -p $out
+              # clean potential removed bots
+              rm -rf $out/*.json
+              for i in ${strings.concatStringsSep " " (lists.map (x: "${getName x},${x}") (attrsets.mapAttrsToList mkBot cfg.bots))}; do IFS=",";
+                set -- $i
+                ln -fs $2 $out/$1
+              done
+            '';
+            replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
+          in
+          ''
+            mkdir -p config
+
+            cp --no-preserve=mode ${asf-config} config/ASF.json
+
+            ${optionalString (cfg.ipcPasswordFile != null) ''
+              ${replaceSecretBin} '#ipcPassword#' '${cfg.ipcPasswordFile}' config/ASF.json
+            ''}
+
+            ${optionalString (cfg.ipcSettings != {}) ''
+              ln -fs ${ipc-config} config/IPC.config
+            ''}
+
+            ${optionalString (cfg.ipcSettings != {}) ''
+              ln -fs ${createBotsScript}/* config/
+            ''}
+
+            rm -f www
+            ${optionalString cfg.web-ui.enable ''
+              ln -s ${cfg.web-ui.package}/lib/dist www
+            ''}
+          '';
       };
     };
   };
 
   meta = {
     buildDocsInSandbox = false;
-    maintainers = with maintainers; [ lom ];
+    maintainers = with maintainers; [ lom SuperSandro2000 ];
   };
 }
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index 96fcd6d2c8b30..bb6898a08c52b 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -53,6 +53,14 @@ in
         '';
       };
 
+      bind = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          The address to which the service should bind.
+        '';
+      };
+
       admins = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -79,6 +87,18 @@ in
           a new map with default settings will be generated before starting the service.
         '';
       };
+      loadLatestSave = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Load the latest savegame on startup. This overrides saveName, in that the latest
+          save will always be used even if a saved game of the given name exists. It still
+          controls the 'canonical' name of the savegame.
+
+          Set this to true to have the server automatically reload a recent autosave after
+          a crash or desync.
+        '';
+      };
       # TODO Add more individual settings as nixos-options?
       # TODO XXX The server tries to copy a newly created config file over the old one
       #   on shutdown, but fails, because it's in the nix store. When is this needed?
@@ -241,8 +261,10 @@ in
           "${cfg.package}/bin/factorio"
           "--config=${cfg.configFile}"
           "--port=${toString cfg.port}"
-          "--start-server=${mkSavePath cfg.saveName}"
+          "--bind=${cfg.bind}"
+          (optionalString (!cfg.loadLatestSave) "--start-server=${mkSavePath cfg.saveName}")
           "--server-settings=${serverSettingsFile}"
+          (optionalString cfg.loadLatestSave "--start-server-load-latest")
           (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
           (optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
         ];
diff --git a/nixos/modules/services/games/minecraft-server.nix b/nixos/modules/services/games/minecraft-server.nix
index 5bb8eff576292..309fe43eaf4c4 100644
--- a/nixos/modules/services/games/minecraft-server.nix
+++ b/nixos/modules/services/games/minecraft-server.nix
@@ -22,6 +22,15 @@ let
   '' + concatStringsSep "\n" (mapAttrsToList
     (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
 
+  stopScript = pkgs.writeShellScript "minecraft-server-stop" ''
+    echo stop > ${config.systemd.sockets.minecraft-server.socketConfig.ListenFIFO}
+
+    # Wait for the PID of the minecraft server to disappear before
+    # returning, so systemd doesn't attempt to SIGKILL it.
+    while kill -0 "$1" 2> /dev/null; do
+      sleep 1s
+    done
+  '';
 
   # To be able to open the firewall, we need to read out port values in the
   # server properties, but fall back to the defaults when those don't exist.
@@ -153,7 +162,7 @@ in {
         type = types.separatedString " ";
         default = "-Xmx2048M -Xms2048M";
         # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
-        example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+        example = "-Xms4092M -Xmx4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
           + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
           + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
         description = "JVM options for the Minecraft server.";
@@ -172,16 +181,35 @@ in {
     };
     users.groups.minecraft = {};
 
+    systemd.sockets.minecraft-server = {
+      bindsTo = [ "minecraft-server.service" ];
+      socketConfig = {
+        ListenFIFO = "/run/minecraft-server.stdin";
+        SocketMode = "0660";
+        SocketUser = "minecraft";
+        SocketGroup = "minecraft";
+        RemoveOnStop = true;
+        FlushPending = true;
+      };
+    };
+
     systemd.services.minecraft-server = {
       description   = "Minecraft Server Service";
       wantedBy      = [ "multi-user.target" ];
-      after         = [ "network.target" ];
+      requires      = [ "minecraft-server.socket" ];
+      after         = [ "network.target" "minecraft-server.socket" ];
 
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+        ExecStop = "${stopScript} $MAINPID";
         Restart = "always";
         User = "minecraft";
         WorkingDirectory = cfg.dataDir;
+
+        StandardInput = "socket";
+        StandardOutput = "journal";
+        StandardError = "journal";
+
         # Hardening
         CapabilityBoundingSet = [ "" ];
         DeviceAllow = [ "" ];
diff --git a/nixos/modules/services/hardware/argonone.nix b/nixos/modules/services/hardware/argonone.nix
new file mode 100644
index 0000000000000..638181b1b12e2
--- /dev/null
+++ b/nixos/modules/services/hardware/argonone.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hardware.argonone;
+in
+{
+  options.services.hardware.argonone = {
+    enable = lib.mkEnableOption "the driver for Argon One Raspberry Pi case fan and power button";
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.argononed;
+      defaultText = "pkgs.argononed";
+      description = ''
+        The package implementing the Argon One driver
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware.i2c.enable = true;
+    hardware.deviceTree.overlays = [
+      {
+        name = "argononed";
+        dtboFile = "${cfg.package}/boot/overlays/argonone.dtbo";
+      }
+      {
+        name = "i2c1-okay-overlay";
+        dtsText = ''
+          /dts-v1/;
+          /plugin/;
+          / {
+            compatible = "brcm,bcm2711";
+            fragment@0 {
+              target = <&i2c1>;
+              __overlay__ {
+                status = "okay";
+              };
+            };
+          };
+        '';
+      }
+    ];
+    environment.systemPackages = [ cfg.package ];
+    systemd.services.argononed = {
+      description = "Argon One Raspberry Pi case Daemon Service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${cfg.package}/bin/argononed";
+        PIDFile = "/run/argononed.pid";
+        Restart = "on-failure";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ misterio77 ];
+
+}
diff --git a/nixos/modules/services/hardware/illum.nix b/nixos/modules/services/hardware/illum.nix
index ff73c99a65376..7f7a850002342 100644
--- a/nixos/modules/services/hardware/illum.nix
+++ b/nixos/modules/services/hardware/illum.nix
@@ -28,6 +28,7 @@ in {
       description = "Backlight Adjustment Service";
       wantedBy = [ "multi-user.target" ];
       serviceConfig.ExecStart = "${pkgs.illum}/bin/illum-d";
+      serviceConfig.Restart = "on-failure";
     };
 
   };
diff --git a/nixos/modules/services/hardware/joycond.nix b/nixos/modules/services/hardware/joycond.nix
index ffef4f8a4e188..d81c1bb6d63d0 100644
--- a/nixos/modules/services/hardware/joycond.nix
+++ b/nixos/modules/services/hardware/joycond.nix
@@ -22,13 +22,9 @@ with lib;
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [
-      kernelPackages.hid-nintendo
-      cfg.package
-    ];
+    environment.systemPackages = [ cfg.package ];
 
-    boot.extraModulePackages = [ kernelPackages.hid-nintendo ];
-    boot.kernelModules = [ "hid_nintendo" ];
+    boot.extraModulePackages = optional (versionOlder kernelPackages.kernel.version "5.16") kernelPackages.hid-nintendo;
 
     services.udev.packages = [ cfg.package ];
 
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 61448af2d33b8..2e9deebbb74ab 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -8,6 +8,24 @@ let
 
   cfg = config.services.udev;
 
+  initrdUdevRules = pkgs.runCommand "initrd-udev-rules" {} ''
+    mkdir -p $out/etc/udev/rules.d
+    for f in 60-cdrom_id 60-persistent-storage 75-net-description 80-drivers 80-net-setup-link; do
+      ln -s ${config.boot.initrd.systemd.package}/lib/udev/rules.d/$f.rules $out/etc/udev/rules.d
+    done
+  '';
+
+
+  # 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.
+  # TODO move this into the initrd-network module when it exists
+  initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} ''
+    mkdir -p $out
+    ln -s ${udev}/lib/systemd/network/*.link $out/
+    ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))}
+  '';
+
   extraUdevRules = pkgs.writeTextFile {
     name = "extra-udev-rules";
     text = cfg.extraRules;
@@ -23,17 +41,16 @@ let
   nixosRules = ''
     # Miscellaneous devices.
     KERNEL=="kvm",                  MODE="0666"
-    KERNEL=="kqemu",                MODE="0666"
 
     # Needed for gpm.
     SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
   '';
 
   # Perform substitutions in all udev rules files.
-  udevRules = pkgs.runCommand "udev-rules"
+  udevRulesFor = { name, udevPackages, udevPath, udev, systemd, binPackages, initrdBin ? null }: pkgs.runCommand name
     { preferLocalBuild = true;
       allowSubstitutes = false;
-      packages = unique (map toString cfg.packages);
+      packages = unique (map toString udevPackages);
     }
     ''
       mkdir -p $out
@@ -61,6 +78,9 @@ let
           --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
           --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
           --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
+      ${optionalString (initrdBin != null) ''
+        substituteInPlace $i --replace '/run/current-system/systemd' "${removeSuffix "/bin" initrdBin}"
+      ''}
       done
 
       echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
@@ -85,8 +105,9 @@ let
       for i in $import_progs $run_progs; do
         # if the path refers to /run/current-system/systemd, replace with config.systemd.package
         if [[ $i == /run/current-system/systemd* ]]; then
-          i="${config.systemd.package}/''${i#/run/current-system/systemd/}"
+          i="${systemd}/''${i#/run/current-system/systemd/}"
         fi
+
         if [[ ! -x $i ]]; then
           echo "FAIL"
           echo "$i is called in udev rules but is not executable or does not exist"
@@ -103,7 +124,7 @@ let
         echo "Consider fixing the following udev rules:"
         echo "$filesToFixup" | while read localFile; do
           remoteFile="origin unknown"
-          for i in ${toString cfg.packages}; do
+          for i in ${toString binPackages}; do
             for j in "$i"/*/udev/rules.d/*; do
               [ -e "$out/$(basename "$j")" ] || continue
               [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
@@ -126,7 +147,7 @@ let
       ${optionalString (!config.boot.hardwareScan) ''
         ln -s /dev/null $out/80-drivers.rules
       ''}
-    ''; # */
+    '';
 
   hwdbBin = pkgs.runCommand "hwdb.bin"
     { preferLocalBuild = true;
@@ -150,6 +171,11 @@ let
       mv etc/udev/hwdb.bin $out
     '';
 
+  compressFirmware = if config.boot.kernelPackages.kernelAtLeast "5.3" then
+    pkgs.compressFirmwareXz
+  else
+    id;
+
   # Udev has a 512-character limit for ENV{PATH}, so create a symlink
   # tree to work around this.
   udevPath = pkgs.buildEnv {
@@ -202,20 +228,6 @@ 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 = ''
@@ -260,7 +272,7 @@ in
       '';
       apply = list: pkgs.buildEnv {
         name = "firmware";
-        paths = list;
+        paths = map compressFirmware list;
         pathsToLink = [ "/lib/firmware" ];
         ignoreCollisions = true;
       };
@@ -283,6 +295,52 @@ in
       '';
     };
 
+    boot.initrd.services.udev = {
+
+      packages = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        visible = false;
+        description = ''
+          <emphasis>This will only be used when systemd is used in stage 1.</emphasis>
+
+          List of packages containing <command>udev</command> rules that will be copied to stage 1.
+          All files found in
+          <filename><replaceable>pkg</replaceable>/etc/udev/rules.d</filename> and
+          <filename><replaceable>pkg</replaceable>/lib/udev/rules.d</filename>
+          will be included.
+        '';
+      };
+
+      binPackages = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        visible = false;
+        description = ''
+          <emphasis>This will only be used when systemd is used in stage 1.</emphasis>
+
+          Packages to search for binaries that are referenced by the udev rules in stage 1.
+          This list always contains /bin of the initrd.
+        '';
+        apply = map getBin;
+      };
+
+      rules = 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.
+        '';
+      };
+
+    };
+
   };
 
 
@@ -298,16 +356,61 @@ in
 
     boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
 
-    boot.initrd.extraUdevRulesCommands = optionalString (cfg.initrdRules != "")
+    boot.initrd.extraUdevRulesCommands = optionalString (!config.boot.initrd.systemd.enable && config.boot.initrd.services.udev.rules != "")
       ''
         cat <<'EOF' > $out/99-local.rules
-        ${cfg.initrdRules}
+        ${config.boot.initrd.services.udev.rules}
         EOF
       '';
 
+    boot.initrd.systemd.additionalUpstreamUnits = [
+      # TODO: "initrd-udevadm-cleanup-db.service" is commented out because of https://github.com/systemd/systemd/issues/12953
+      "systemd-udevd-control.socket"
+      "systemd-udevd-kernel.socket"
+      "systemd-udevd.service"
+      "systemd-udev-settle.service"
+      "systemd-udev-trigger.service"
+    ];
+    boot.initrd.systemd.storePaths = [
+      "${config.boot.initrd.systemd.package}/lib/systemd/systemd-udevd"
+      "${config.boot.initrd.systemd.package}/lib/udev/ata_id"
+      "${config.boot.initrd.systemd.package}/lib/udev/cdrom_id"
+      "${config.boot.initrd.systemd.package}/lib/udev/scsi_id"
+      "${config.boot.initrd.systemd.package}/lib/udev/rules.d"
+    ] ++ map (x: "${x}/bin") config.boot.initrd.services.udev.binPackages;
+
+    # Generate the udev rules for the initrd
+    boot.initrd.systemd.contents = {
+      "/etc/udev/rules.d".source = udevRulesFor {
+        name = "initrd-udev-rules";
+        initrdBin = config.boot.initrd.systemd.contents."/bin".source;
+        udevPackages = config.boot.initrd.services.udev.packages;
+        udevPath = config.boot.initrd.systemd.contents."/bin".source;
+        udev = config.boot.initrd.systemd.package;
+        systemd = config.boot.initrd.systemd.package;
+        binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
+      };
+      "/etc/systemd/network".source = initrdLinkUnits;
+    };
+    # Insert initrd rules
+    boot.initrd.services.udev.packages = [
+      initrdUdevRules
+      (mkIf (config.boot.initrd.services.udev.rules != "") (pkgs.writeTextFile {
+        name = "initrd-udev-rules";
+        destination = "/etc/udev/rules.d/99-local.rules";
+        text = config.boot.initrd.services.udev.rules;
+      }))
+    ];
+
     environment.etc =
       {
-        "udev/rules.d".source = udevRules;
+        "udev/rules.d".source = udevRulesFor {
+          name = "udev-rules";
+          udevPackages = cfg.packages;
+          systemd = config.systemd.package;
+          binPackages = cfg.packages;
+          inherit udevPath udev;
+        };
         "udev/hwdb.bin".source = hwdbBin;
       };
 
@@ -338,4 +441,8 @@ in
       };
 
   };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "udev" "initrdRules" ] [ "boot" "initrd" "services" "udev" "rules" ])
+  ];
 }
diff --git a/nixos/modules/services/hardware/udisks2.nix b/nixos/modules/services/hardware/udisks2.nix
index e898f32605856..ea552ce867e87 100644
--- a/nixos/modules/services/hardware/udisks2.nix
+++ b/nixos/modules/services/hardware/udisks2.nix
@@ -4,6 +4,13 @@
 
 with lib;
 
+let
+  settingsFormat = pkgs.formats.ini {
+    listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
+  };
+  configFiles = mapAttrs (name: value: (settingsFormat.generate name value)) (mapAttrs' (name: value: nameValuePair name value ) config.services.udisks2.settings);
+in
+
 {
 
   ###### interface
@@ -21,6 +28,36 @@ with lib;
         '';
       };
 
+      settings = mkOption rec {
+        type = types.attrsOf settingsFormat.type;
+        apply = recursiveUpdate default;
+        default = {
+          "udisks2.conf" = {
+            udisks2 = {
+              modules = [ "*" ];
+              modules_load_preference = "ondemand";
+            };
+            defaults = {
+              encryption = "luks2";
+            };
+          };
+        };
+        example = literalExpression ''
+        {
+          "WDC-WD10EZEX-60M2NA0-WD-WCC3F3SJ0698.conf" = {
+            ATA = {
+              StandbyTimeout = 50;
+            };
+          };
+        };
+        '';
+        description = ''
+          Options passed to udisksd.
+          See <link xlink:href="http://manpages.ubuntu.com/manpages/latest/en/man5/udisks2.conf.5.html">here</link> and
+          drive configuration in <link xlink:href="http://manpages.ubuntu.com/manpages/latest/en/man8/udisks.8.html">here</link> for supported options.
+        '';
+      };
+
     };
 
   };
@@ -32,6 +69,10 @@ with lib;
 
     environment.systemPackages = [ pkgs.udisks2 ];
 
+    environment.etc = mapAttrs' (name: value: nameValuePair "udisks2/${name}" { source = value; } ) configFiles;
+
+    security.polkit.enable = true;
+
     services.dbus.packages = [ pkgs.udisks2 ];
 
     systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ];
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index 212c0227c0d0a..a743bbf21c8c2 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -164,8 +164,6 @@ in
     environment.systemPackages = [ cfg.package ];
 
     systemd.services.undervolt = {
-      path = [ pkgs.undervolt ];
-
       description = "Intel Undervolting Service";
 
       # Apply undervolt on boot, nixos generation switch and resume
@@ -175,7 +173,7 @@ in
       serviceConfig = {
         Type = "oneshot";
         Restart = "no";
-        ExecStart = "${pkgs.undervolt}/bin/undervolt ${toString cliArgs}";
+        ExecStart = "${cfg.package}/bin/undervolt ${toString cliArgs}";
       };
     };
 
diff --git a/nixos/modules/services/hardware/usbrelayd.nix b/nixos/modules/services/hardware/usbrelayd.nix
new file mode 100644
index 0000000000000..2cee4e1ff7e8c
--- /dev/null
+++ b/nixos/modules/services/hardware/usbrelayd.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.usbrelayd;
+in
+{
+  options.services.usbrelayd = with types; {
+    enable = mkEnableOption "USB Relay MQTT daemon";
+
+    broker = mkOption {
+      type = str;
+      description = "Hostname or IP address of your MQTT Broker.";
+      default = "127.0.0.1";
+      example = [
+        "mqtt"
+        "192.168.1.1"
+      ];
+    };
+
+    clientName = mkOption {
+      type = str;
+      description = "Name, your client connects as.";
+      default = "MyUSBRelay";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc."usbrelayd.conf".text = ''
+      [MQTT]
+      BROKER = ${cfg.broker}
+      CLIENTNAME = ${cfg.clientName}
+    '';
+
+    services.udev.packages = [ pkgs.usbrelayd ];
+    systemd.packages = [ pkgs.usbrelayd ];
+    users.users.usbrelay = {
+      isSystemUser = true;
+      group = "usbrelay";
+    };
+    users.groups.usbrelay = { };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ wentasah ];
+  };
+}
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
new file mode 100644
index 0000000000000..2cff5051c757f
--- /dev/null
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -0,0 +1,573 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.home-assistant;
+  format = pkgs.formats.yaml {};
+
+  # Render config attribute sets to YAML
+  # Values that are null will be filtered from the output, so this is one way to have optional
+  # options shown in settings.
+  # We post-process the result to add support for YAML functions, like secrets or includes, see e.g.
+  # https://www.home-assistant.io/docs/configuration/secrets/
+  filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) cfg.config or {};
+  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
+    cp ${format.generate "configuration.yaml" filteredConfig} $out
+    sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
+  '';
+  lovelaceConfig = cfg.lovelaceConfig or {};
+  lovelaceConfigFile = format.generate "ui-lovelace.yaml" lovelaceConfig;
+
+  # Components advertised by the home-assistant package
+  availableComponents = cfg.package.availableComponents;
+
+  # Components that were added by overriding the package
+  explicitComponents = cfg.package.extraComponents;
+  useExplicitComponent = component: elem component explicitComponents;
+
+  # Given a component "platform", looks up whether it is used in the config
+  # as `platform = "platform";`.
+  #
+  # For example, the component mqtt.sensor is used as follows:
+  # config.sensor = [ {
+  #   platform = "mqtt";
+  #   ...
+  # } ];
+  usedPlatforms = config:
+    if isAttrs config then
+      optional (config ? platform) config.platform
+      ++ concatMap usedPlatforms (attrValues config)
+    else if isList config then
+      concatMap usedPlatforms config
+    else [ ];
+
+  useComponentPlatform = component: elem component (usedPlatforms cfg.config);
+
+  # Returns whether component is used in config, explicitly passed into package or
+  # configured in the module.
+  useComponent = component:
+    hasAttrByPath (splitString "." component) cfg.config
+    || useComponentPlatform component
+    || useExplicitComponent component
+    || builtins.elem component cfg.extraComponents;
+
+  # Final list of components passed into the package to include required dependencies
+  extraComponents = filter useComponent availableComponents;
+
+  package = (cfg.package.override (oldArgs: {
+    # Respect overrides that already exist in the passed package and
+    # concat it with values passed via the module.
+    extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
+    extraPackages = ps: (oldArgs.extraPackages or (_: []) ps) ++ (cfg.extraPackages ps);
+  }));
+in {
+  imports = [
+    # Migrations in NixOS 22.05
+    (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
+    (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
+    (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
+  ];
+
+  meta = {
+    buildDocsInSandbox = false;
+    maintainers = teams.home-assistant.members;
+  };
+
+  options.services.home-assistant = {
+    # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
+    # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
+    enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
+
+    configDir = mkOption {
+      default = "/var/lib/hass";
+      type = types.path;
+      description = "The config directory, where your <filename>configuration.yaml</filename> is located.";
+    };
+
+    extraComponents = mkOption {
+      type = types.listOf (types.enum availableComponents);
+      default = [
+        # List of components required to complete the onboarding
+        "default_config"
+        "met"
+        "esphome"
+      ] ++ optionals (pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64) [
+        # Use the platform as an indicator that we might be running on a RaspberryPi and include
+        # relevant components
+        "rpi_power"
+      ];
+      example = literalExpression ''
+        [
+          "analytics"
+          "default_config"
+          "esphome"
+          "my"
+          "shopping_list"
+          "wled"
+        ]
+      '';
+      description = ''
+        List of <link xlink:href="https://www.home-assistant.io/integrations/">components</link> that have their dependencies included in the package.
+
+        The component name can be found in the URL, for example <literal>https://www.home-assistant.io/integrations/ffmpeg/</literal> would map to <literal>ffmpeg</literal>.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = _: [];
+      defaultText = literalExpression ''
+        python3Packages: with python3Packages; [];
+      '';
+      example = literalExpression ''
+        python3Packages: with python3Packages; [
+          # postgresql support
+          psycopg2
+        ];
+      '';
+      description = ''
+        List of packages to add to propagatedBuildInputs.
+
+        A popular example is <package>python3Packages.psycopg2</package>
+        for PostgreSQL support in the recorder component.
+      '';
+    };
+
+    config = mkOption {
+      type = types.nullOr (types.submodule {
+        freeformType = format.type;
+        options = {
+          # This is a partial selection of the most common options, so new users can quickly
+          # pick up how to match home-assistants config structure to ours. It also lets us preset
+          # config values intelligently.
+
+          homeassistant = {
+            # https://www.home-assistant.io/docs/configuration/basic/
+            name = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "Home";
+              description = ''
+                Name of the location where Home Assistant is running.
+              '';
+            };
+
+            latitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 52.3;
+              description = ''
+                Latitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            longitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 4.9;
+              description = ''
+                Longitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            unit_system = mkOption {
+              type = types.nullOr (types.enum [ "metric" "imperial" ]);
+              default = null;
+              example = "metric";
+              description = ''
+                The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
+              '';
+            };
+
+            temperature_unit = mkOption {
+              type = types.nullOr (types.enum [ "C" "F" ]);
+              default = null;
+              example = "C";
+              description = ''
+                Override temperature unit set by unit_system. <literal>C</literal> for Celsius, <literal>F</literal> for Fahrenheit.
+              '';
+            };
+
+            time_zone = mkOption {
+              type = types.nullOr types.str;
+              default = config.time.timeZone or null;
+              defaultText = literalExpression ''
+                config.time.timeZone or null
+              '';
+              example = "Europe/Amsterdam";
+              description = ''
+                Pick your time zone from the column TZ of Wikipedia’s <link xlink:href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">list of tz database time zones</link>.
+              '';
+            };
+          };
+
+          http = {
+            # https://www.home-assistant.io/integrations/http/
+            server_host = mkOption {
+              type = types.either types.str (types.listOf types.str);
+              default = [
+                "0.0.0.0"
+                "::"
+              ];
+              example = "::1";
+              description = ''
+                Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
+              '';
+            };
+
+            server_port = mkOption {
+              default = 8123;
+              type = types.port;
+              description = ''
+                The port on which to listen.
+              '';
+            };
+          };
+
+          lovelace = {
+            # https://www.home-assistant.io/lovelace/dashboards/
+            mode = mkOption {
+              type = types.enum [ "yaml" "storage" ];
+              default = if cfg.lovelaceConfig != null
+                then "yaml"
+                else "storage";
+              defaultText = literalExpression ''
+                if cfg.lovelaceConfig != null
+                  then "yaml"
+                else "storage";
+              '';
+              example = "yaml";
+              description = ''
+                In what mode should the main Lovelace panel be, <literal>yaml</literal> or <literal>storage</literal> (UI managed).
+              '';
+            };
+          };
+        };
+      });
+      example = literalExpression ''
+        {
+          homeassistant = {
+            name = "Home";
+            latitude = "!secret latitude";
+            longitude = "!secret longitude";
+            elevation = "!secret elevation";
+            unit_system = "metric";
+            time_zone = "UTC";
+          };
+          frontend = {
+            themes = "!include_dir_merge_named themes";
+          };
+          http = {};
+          feedreader.urls = [ "https://nixos.org/blogs.xml" ];
+        }
+      '';
+      description = ''
+        Your <filename>configuration.yaml</filename> as a Nix attribute set.
+
+        YAML functions like <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">secrets</link>
+        can be passed as a string and will be unquoted automatically.
+
+        Unless this option is explicitly set to <literal>null</literal>
+        we assume your <filename>configuration.yaml</filename> is
+        managed through this module and thereby overwritten on startup.
+      '';
+    };
+
+    configWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>configuration.yaml</filename> writable.
+
+        This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>config</option> is set.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    lovelaceConfig = mkOption {
+      default = null;
+      type = types.nullOr format.type;
+      # from https://www.home-assistant.io/lovelace/dashboards/
+      example = literalExpression ''
+        {
+          title = "My Awesome Home";
+          views = [ {
+            title = "Example";
+            cards = [ {
+              type = "markdown";
+              title = "Lovelace";
+              content = "Welcome to your **Lovelace UI**.";
+            } ];
+          } ];
+        }
+      '';
+      description = ''
+        Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
+        Setting this option will automatically set <literal>lovelace.mode</literal> to <literal>yaml</literal>.
+
+        Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
+      '';
+    };
+
+    lovelaceConfigWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>ui-lovelace.yaml</filename> writable.
+
+        This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>lovelaceConfig</option> is set.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+      defaultText = literalExpression ''
+        pkgs.home-assistant.overrideAttrs (oldAttrs: {
+          doInstallCheck = false;
+        })
+      '';
+      type = types.package;
+      example = literalExpression ''
+        pkgs.home-assistant.override {
+          extraPackages = python3Packages: with python3Packages; [
+            psycopg2
+          ];
+          extraComponents = [
+            "default_config"
+            "esphome"
+            "met"
+          ];
+        }
+      '';
+      description = ''
+        The Home Assistant package to use.
+      '';
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = types.bool;
+      description = "Whether to open the firewall for the specified port.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.openFirewall -> !isNull cfg.config;
+        message = "openFirewall can only be used with a declarative config";
+      }
+    ];
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ];
+
+    # symlink the configuration to /etc/home-assistant
+    environment.etc = lib.mkMerge [
+      (lib.mkIf (cfg.config != null && !cfg.configWritable) {
+        "home-assistant/configuration.yaml".source = configFile;
+      })
+
+      (lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
+        "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
+      })
+    ];
+
+    systemd.services.home-assistant = {
+      description = "Home Assistant";
+      after = [
+        "network-online.target"
+
+        # prevent races with database creation
+        "mysql.service"
+        "postgresql.service"
+      ];
+      reloadTriggers = lib.optional (cfg.config != null) configFile
+      ++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile;
+
+      preStart = let
+        copyConfig = if cfg.configWritable then ''
+          cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/configuration.yaml"
+          ln -s /etc/home-assistant/configuration.yaml "${cfg.configDir}/configuration.yaml"
+        '';
+        copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
+          cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/ui-lovelace.yaml"
+          ln -s /etc/home-assistant/ui-lovelace.yaml "${cfg.configDir}/ui-lovelace.yaml"
+        '';
+      in
+        (optionalString (cfg.config != null) copyConfig) +
+        (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig)
+      ;
+      serviceConfig = let
+        # List of capabilities to equip home-assistant with, depending on configured components
+        capabilities = [
+          # Empty string first, so we will never accidentally have an empty capability bounding set
+          # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
+          ""
+        ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
+          # Required for interaction with hci devices and bluetooth sockets
+          # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
+          "CAP_NET_ADMIN"
+          "CAP_NET_RAW"
+        ] ++ lib.optionals (useComponent "emulated_hue") [
+          # Alexa looks for the service on port 80
+          # https://www.home-assistant.io/integrations/emulated_hue
+          "CAP_NET_BIND_SERVICE"
+        ] ++ lib.optionals (useComponent "nmap_tracker") [
+          # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
+          "CAP_NET_ADMIN"
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ]));
+        componentsUsingBluetooth = [
+          # Components that require the AF_BLUETOOTH address family
+          "bluetooth_tracker"
+          "bluetooth_le_tracker"
+        ];
+        componentsUsingPing = [
+          # Components that require the capset syscall for the ping wrapper
+          "ping"
+          "wake_on_lan"
+        ];
+        componentsUsingSerialDevices = [
+          # Components that require access to serial devices (/dev/tty*)
+          # List generated from home-assistant documentation:
+          #   git clone https://github.com/home-assistant/home-assistant.io/
+          #   cd source/_integrations
+          #   rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
+          # And then extended by references found in the source code, these
+          # mostly the ones using config flows already.
+          "acer_projector"
+          "alarmdecoder"
+          "arduino"
+          "blackbird"
+          "deconz"
+          "dsmr"
+          "edl21"
+          "elkm1"
+          "elv"
+          "enocean"
+          "firmata"
+          "flexit"
+          "gpsd"
+          "insteon"
+          "kwb"
+          "lacrosse"
+          "mhz19"
+          "modbus"
+          "modem_callerid"
+          "mysensors"
+          "nad"
+          "numato"
+          "rflink"
+          "rfxtrx"
+          "scsgate"
+          "serial"
+          "serial_pm"
+          "sms"
+          "upb"
+          "usb"
+          "velbus"
+          "w800rf32"
+          "xbee"
+          "zha"
+          "zwave"
+          "zwave_js"
+        ];
+      in {
+        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "hass";
+        Group = "hass";
+        Restart = "on-failure";
+        RestartForceExitStatus = "100";
+        SuccessExitStatus = "100";
+        KillSignal = "SIGINT";
+
+        # Hardening
+        AmbientCapabilities = capabilities;
+        CapabilityBoundingSet = capabilities;
+        DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
+          "char-ttyACM rw"
+          "char-ttyAMA rw"
+          "char-ttyUSB rw"
+        ]);
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateUsers = false; # prevents gaining capabilities in the host namespace
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "all";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        ReadWritePaths = let
+          # Allow rw access to explicitly configured paths
+          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
+          value = attrByPath cfgPath [] cfg;
+          allowPaths = if isList value then value else singleton value;
+        in [ "${cfg.configDir}" ] ++ allowPaths;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ] ++ optionals (any useComponent componentsUsingBluetooth) [
+          "AF_BLUETOOTH"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ] ++ optionals (any useComponent componentsUsingPing) [
+          "capset"
+        ];
+        UMask = "0077";
+      };
+      path = [
+        "/run/wrappers" # needed for ping
+      ];
+    };
+
+    systemd.targets.home-assistant = rec {
+      description = "Home Assistant";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "home-assistant.service" ];
+      after = wants;
+    };
+
+    users.users.hass = {
+      home = cfg.configDir;
+      createHome = true;
+      group = "hass";
+      uid = config.ids.uids.hass;
+    };
+
+    users.groups.hass.gid = config.ids.gids.hass;
+  };
+}
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/home-automation/zigbee2mqtt.nix
index ff6d595e5a6e3..ff6d595e5a6e3 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/home-automation/zigbee2mqtt.nix
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
index e6a23233ba28d..28e2d18bf0311 100644
--- a/nixos/modules/services/logging/graylog.nix
+++ b/nixos/modules/services/logging/graylog.nix
@@ -132,7 +132,7 @@ in
         description = "Graylog server daemon user";
       };
     };
-    users.groups = mkIf (cfg.user == "graylog") {};
+    users.groups = mkIf (cfg.user == "graylog") { graylog = {}; };
 
     systemd.tmpfiles.rules = [
       "d '${cfg.messageJournalDir}' - ${cfg.user} - - -"
diff --git a/nixos/modules/services/logging/heartbeat.nix b/nixos/modules/services/logging/heartbeat.nix
index 56fb4deabda52..843ec033911f1 100644
--- a/nixos/modules/services/logging/heartbeat.nix
+++ b/nixos/modules/services/logging/heartbeat.nix
@@ -20,6 +20,16 @@ in
 
       enable = mkEnableOption "heartbeat";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.heartbeat;
+        defaultText = literalExpression "pkgs.heartbeat";
+        example = literalExpression "pkgs.heartbeat7";
+        description = ''
+          The heartbeat package to use.
+        '';
+      };
+
       name = mkOption {
         type = types.str;
         default = "heartbeat";
@@ -67,7 +77,7 @@ in
       serviceConfig = {
         User = "nobody";
         AmbientCapabilities = "cap_net_raw";
-        ExecStart = "${pkgs.heartbeat}/bin/heartbeat -c \"${heartbeatYml}\" -path.data \"${cfg.stateDir}/data\" -path.logs \"${cfg.stateDir}/logs\"";
+        ExecStart = "${cfg.package}/bin/heartbeat -c \"${heartbeatYml}\" -path.data \"${cfg.stateDir}/data\" -path.logs \"${cfg.stateDir}/logs\"";
       };
     };
   };
diff --git a/nixos/modules/services/logging/klogd.nix b/nixos/modules/services/logging/klogd.nix
index 8d371c161eb18..1de0e58abbb35 100644
--- a/nixos/modules/services/logging/klogd.nix
+++ b/nixos/modules/services/logging/klogd.nix
@@ -1,38 +1,9 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
+{ lib, ... }:
 
 {
-  ###### interface
-
-  options = {
-
-    services.klogd.enable = mkOption {
-      type = types.bool;
-      default = versionOlder (getVersion config.boot.kernelPackages.kernel) "3.5";
-      defaultText = literalExpression ''versionOlder (getVersion config.boot.kernelPackages.kernel) "3.5"'';
-      description = ''
-        Whether to enable klogd, the kernel log message processing
-        daemon.  Since systemd handles logging of kernel messages on
-        Linux 3.5 and later, this is only useful if you're running an
-        older kernel.
-      '';
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.services.klogd.enable {
-    systemd.services.klogd = {
-      description = "Kernel Log Daemon";
-      wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.sysklogd ];
-      unitConfig.ConditionVirtualization = "!systemd-nspawn";
-      script =
-        "klogd -c 1 -2 -n " +
-        "-k $(dirname $(readlink -f /run/booted-system/kernel))/System.map";
-    };
-  };
+  imports = [
+    (lib.mkRemovedOptionModule [ "security" "klogd" "enable" ] ''
+      Logging of kernel messages is now handled by systemd.
+    '')
+  ];
 }
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index 8cef4e8c083a9..893d951ee2348 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -4,9 +4,11 @@ with lib;
 
 let
   cfg = config.services.logrotate;
-  inherit (config.users) groups;
 
-  pathOpts = { name, ... }:  {
+  # deprecated legacy compat settings
+  # these options will be removed before 22.11 in the following PR:
+  # https://github.com/NixOS/nixpkgs/pull/164169
+  pathOpts = { name, ... }: {
     options = {
       enable = mkOption {
         type = types.bool;
@@ -85,25 +87,116 @@ let
     };
 
     config.name = name;
-    config.extraConfig = ''
-      missingok
-      notifempty
-    '';
   };
 
-  mkConf = pathOpts: ''
-    # generated by NixOS using the `services.logrotate.paths.${pathOpts.name}` attribute set
-    ${concatMapStringsSep " " (path: ''"${path}"'') (toList pathOpts.path)} {
-      ${optionalString (pathOpts.user != null || pathOpts.group != null) "su ${pathOpts.user} ${pathOpts.group}"}
-      ${pathOpts.frequency}
-      rotate ${toString pathOpts.keep}
-      ${pathOpts.extraConfig}
-    }
-  '';
+  generateLine = n: v:
+    if builtins.elem n [ "files" "priority" "enable" "global" ] || v == null then null
+    else if builtins.elem n [ "extraConfig" "frequency" ] then "${v}\n"
+    else if builtins.elem n [ "firstaction" "lastaction" "prerotate" "postrotate" "preremove" ]
+         then "${n}\n    ${v}\n  endscript\n"
+    else if isInt v then "${n} ${toString v}\n"
+    else if v == true then "${n}\n"
+    else if v == false then "no${n}\n"
+    else "${n} ${v}\n";
+  generateSection = indent: settings: concatStringsSep (fixedWidthString indent " " "") (
+    filter (x: x != null) (mapAttrsToList generateLine settings)
+  );
+
+  # generateSection includes a final newline hence weird closing brace
+  mkConf = settings:
+    if settings.global or false then generateSection 0 settings
+    else ''
+      ${concatMapStringsSep "\n" (files: ''"${files}"'') (toList settings.files)} {
+        ${generateSection 2 settings}}
+    '';
+
+  # below two mapPaths are compat functions
+  mapPathOptToSetting = n: v:
+    if n == "keep" then nameValuePair "rotate" v
+    else if n == "path" then nameValuePair "files" v
+    else nameValuePair n v;
 
-  paths = sortProperties (attrValues (filterAttrs (_: pathOpts: pathOpts.enable) cfg.paths));
-  configFile = pkgs.writeText "logrotate.conf" (concatStringsSep "\n" ((map mkConf paths) ++ [ cfg.extraConfig ]));
+  mapPathsToSettings = path: pathOpts:
+    nameValuePair path (
+      filterAttrs (n: v: ! builtins.elem n [ "user" "group" "name" ] && v != "") (
+        (mapAttrs' mapPathOptToSetting pathOpts) //
+        {
+          su =
+            if pathOpts.user != null
+            then "${pathOpts.user} ${pathOpts.group}"
+            else null;
+        }
+      )
+    );
+
+  settings = sortProperties (attrValues (filterAttrs (_: settings: settings.enable) (
+    foldAttrs recursiveUpdate { } [
+      {
+        header = {
+          enable = true;
+          missingok = true;
+          notifempty = true;
+          frequency = "weekly";
+          rotate = 4;
+        };
+        # compat section
+        extraConfig = {
+          enable = (cfg.extraConfig != "");
+          global = true;
+          extraConfig = cfg.extraConfig;
+          priority = 101;
+        };
+      }
+      (mapAttrs' mapPathsToSettings cfg.paths)
+      cfg.settings
+      { header = { global = true; priority = 100; }; }
+    ]
+  )));
+  configFile = pkgs.writeTextFile {
+    name = "logrotate.conf";
+    text = concatStringsSep "\n" (
+      map mkConf settings
+    );
+    checkPhase = optionalString cfg.checkConfig ''
+      # logrotate --debug also checks that users specified in config
+      # file exist, but we only have sandboxed users here so brown these
+      # out. according to man page that means su, create and createolddir.
+      # files required to exist also won't be present, so missingok is forced.
+      user=$(${pkgs.buildPackages.coreutils}/bin/id -un)
+      group=$(${pkgs.buildPackages.coreutils}/bin/id -gn)
+      sed -e "s/\bsu\s.*/su $user $group/" \
+          -e "s/\b\(create\s\+[0-9]*\s*\|createolddir\s\+[0-9]*\s\+\).*/\1$user $group/" \
+          -e "1imissingok" -e "s/\bnomissingok\b//" \
+          $out > logrotate.conf
+      # Since this makes for very verbose builds only show real error.
+      # There is no way to control log level, but logrotate hardcodes
+      # 'error:' at common log level, so we can use grep, taking care
+      # to keep error codes
+      set -o pipefail
+      if ! ${pkgs.buildPackages.logrotate}/sbin/logrotate -s logrotate.status \
+                      --debug logrotate.conf 2>&1 \
+                  | ( ! grep "error:" ) > logrotate-error; then
+              echo "Logrotate configuration check failed."
+              echo "The failing configuration (after adjustments to pass tests in sandbox) was:"
+              printf "%s\n" "-------"
+              cat logrotate.conf
+              printf "%s\n" "-------"
+              echo "The error reported by logrotate was as follow:"
+              printf "%s\n" "-------"
+              cat logrotate-error
+              printf "%s\n" "-------"
+              echo "You can disable this check with services.logrotate.checkConfig = false,"
+              echo "but if you think it should work please report this failure along with"
+              echo "the config file being tested!"
+              false
+      fi
+    '';
+  };
 
+  mailOption =
+    if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)
+    then "--mail=${pkgs.mailutils}/bin/mail"
+    else "";
 in
 {
   imports = [
@@ -112,15 +205,122 @@ in
 
   options = {
     services.logrotate = {
-      enable = mkEnableOption "the logrotate systemd service";
+      enable = mkEnableOption "the logrotate systemd service" // {
+        default = foldr (n: a: a || n.enable) false (attrValues cfg.settings);
+        defaultText = literalExpression "cfg.settings != {}";
+      };
+
+      settings = mkOption {
+        default = { };
+        description = ''
+          logrotate freeform settings: each attribute here will define its own section,
+          ordered by priority, which can either define files to rotate with their settings
+          or settings common to all further files settings.
+          Refer to <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+        '';
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = with types; attrsOf (nullOr (oneOf [ int bool str ]));
 
+          options = {
+            enable = mkEnableOption "setting individual kill switch" // {
+              default = true;
+            };
+
+            global = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether this setting is a global option or not: set to have these
+                settings apply to all files settings with a higher priority.
+              '';
+            };
+            files = mkOption {
+              type = with types; either str (listOf str);
+              default = name;
+              defaultText = ''
+                The attrset name if not specified
+              '';
+              description = ''
+                Single or list of files for which rules are defined.
+                The files are quoted with double-quotes in logrotate configuration,
+                so globs and spaces are supported.
+                Note this setting is ignored if globals is true.
+              '';
+            };
+
+            frequency = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                How often to rotate the logs. Defaults to previously set global setting,
+                which itself defauts to weekly.
+              '';
+            };
+
+            priority = mkOption {
+              type = types.int;
+              default = 1000;
+              description = ''
+                Order of this logrotate block in relation to the others. The semantics are
+                the same as with `lib.mkOrder`. Smaller values are inserted first.
+              '';
+            };
+          };
+
+        }));
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = configFile;
+        defaultText = ''
+          A configuration file automatically generated by NixOS.
+        '';
+        description = ''
+          Override the configuration file used by MySQL. By default,
+          NixOS generates one automatically from <xref linkend="opt-services.logrotate.settings"/>.
+        '';
+        example = literalExpression ''
+          pkgs.writeText "logrotate.conf" '''
+            missingok
+            "/var/log/*.log" {
+              rotate 4
+              weekly
+            }
+          ''';
+        '';
+      };
+
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the config should be checked at build time.
+
+          Some options are not checkable at build time because of the build sandbox:
+          for example, the test does not know about existing files and system users are
+          not known.
+          These limitations mean we must adjust the file for tests (missingok is forced
+          and users are replaced by dummy users), so tests are complemented by a
+          logrotate-checkconf service that is enabled by default.
+          This extra check can be disabled by disabling it at the systemd level with the
+          <option>services.systemd.services.logrotate-checkconf.enable</option> option.
+
+          Conversely there are still things that might make this check fail incorrectly
+          (e.g. a file path where we don't have access to intermediate directories):
+          in this case you can disable the failing check with this option.
+        '';
+      };
+
+      # deprecated legacy compat settings
       paths = mkOption {
         type = with types; attrsOf (submodule pathOpts);
-        default = {};
+        default = { };
         description = ''
           Attribute set of paths to rotate. The order each block appears in the generated configuration file
           can be controlled by the <link linkend="opt-services.logrotate.paths._name_.priority">priority</link> option
           using the same semantics as `lib.mkOrder`. Smaller values have a greater priority.
+          This setting has been deprecated in favor of <link linkend="opt-services.logrotate.settings">logrotate settings</link>.
         '';
         example = literalExpression ''
           {
@@ -149,48 +349,55 @@ in
         description = ''
           Extra contents to append to the logrotate configuration file. Refer to
           <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+          This setting has been deprecated in favor of
+          <link linkend="opt-services.logrotate.settings">logrotate settings</link>.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
-    assertions = mapAttrsToList (name: pathOpts:
-      { assertion = (pathOpts.user != null) == (pathOpts.group != null);
-        message = ''
-          If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
-        '';
-      }
-    ) cfg.paths;
+    assertions =
+      mapAttrsToList
+        (name: pathOpts:
+          {
+            assertion = (pathOpts.user != null) == (pathOpts.group != null);
+            message = ''
+              If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
+            '';
+          })
+        cfg.paths;
 
-    services.logrotate = {
-      paths = {
-        "/var/log/btmp" = {
-          frequency = mkDefault "monthly";
-          keep = mkDefault 1;
-          extraConfig = ''
-            create 0660 root ${groups.utmp.name}
-          '';
-        };
-        "/var/log/wtmp" = {
-          frequency = mkDefault "monthly";
-          keep = mkDefault 1;
-          extraConfig = ''
-            create 0664 root ${groups.utmp.name}
-          '';
-        };
-      };
-    };
+    warnings =
+      (mapAttrsToList
+        (name: pathOpts: ''
+          Using config.services.logrotate.paths.${name} is deprecated and will become unsupported in a future release.
+          Please use services.logrotate.settings instead.
+        '')
+        cfg.paths
+      ) ++
+      (optional (cfg.extraConfig != "") ''
+        Using config.services.logrotate.extraConfig is deprecated and will become unsupported in a future release.
+        Please use services.logrotate.settings with globals=true instead.
+      '');
 
     systemd.services.logrotate = {
       description = "Logrotate Service";
-      wantedBy = [ "multi-user.target" ];
       startAt = "hourly";
 
       serviceConfig = {
         Restart = "no";
         User = "root";
-        ExecStart = "${pkgs.logrotate}/sbin/logrotate ${configFile}";
+        ExecStart = "${pkgs.logrotate}/sbin/logrotate ${mailOption} ${cfg.configFile}";
+      };
+    };
+    systemd.services.logrotate-checkconf = {
+      description = "Logrotate configuration check";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.logrotate}/sbin/logrotate --debug ${cfg.configFile}";
       };
     };
   };
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index a08203dffe789..5d00feabe1c1d 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -109,7 +109,7 @@ in
           '''
             # Read from journal
             pipe {
-              command => "''${pkgs.systemd}/bin/journalctl -f -o json"
+              command => "''${config.systemd.package}/bin/journalctl -f -o json"
               type => "syslog" codec => json {}
             }
           '''
diff --git a/nixos/modules/services/logging/syslog-ng.nix b/nixos/modules/services/logging/syslog-ng.nix
index 0a57bf20bd071..1c11de51f2c76 100644
--- a/nixos/modules/services/logging/syslog-ng.nix
+++ b/nixos/modules/services/logging/syslog-ng.nix
@@ -51,9 +51,6 @@ in {
       extraModulePaths = mkOption {
         type = types.listOf types.str;
         default = [];
-        example = literalExpression ''
-          [ "''${pkgs.syslogng_incubator}/lib/syslog-ng" ]
-        '';
         description = ''
           A list of paths that should be included in syslog-ng's
           <literal>--module-path</literal> option. They should usually
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 0c9b38b44b24d..f08605218a535 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -6,10 +6,10 @@ let
 
   cfg = config.services.mailman;
 
-  pythonEnv = pkgs.python3.withPackages (ps:
-    [ps.mailman ps.mailman-web]
-    ++ lib.optional cfg.hyperkitty.enable ps.mailman-hyperkitty
-    ++ cfg.extraPythonPackages);
+  inherit (pkgs.mailmanPackages.buildEnvs { withHyperkitty = cfg.hyperkitty.enable; withLDAP = cfg.ldap.enable; })
+    mailmanEnv webEnv;
+
+  withPostgresql = config.services.postgresql.enable;
 
   # This deliberately doesn't use recursiveUpdate so users can
   # override the defaults.
@@ -44,7 +44,13 @@ let
     transport_file_type: hash
   '';
 
-  mailmanCfg = lib.generators.toINI {} cfg.settings;
+  mailmanCfg = lib.generators.toINI {}
+    (recursiveUpdate cfg.settings
+      ((optionalAttrs (cfg.restApiPassFile != null) {
+        webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#";
+      })));
+
+  mailmanCfgFile = pkgs.writeText "mailman-raw.cfg" mailmanCfg;
 
   mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
@@ -72,6 +78,9 @@ in {
       stored in the world-readable Nix store.  To continue using
       Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
     '')
+    (mkRemovedOptionModule [ "services" "mailman" "package" ] ''
+      Didn't have an effect for several years.
+    '')
   ];
 
   options = {
@@ -84,12 +93,112 @@ in {
         description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
       };
 
-      package = mkOption {
-        type = types.package;
-        default = pkgs.mailman;
-        defaultText = literalExpression "pkgs.mailman";
-        example = literalExpression "pkgs.mailman.override { archivers = []; }";
-        description = "Mailman package to use";
+      ldap = {
+        enable = mkEnableOption "LDAP auth";
+        serverUri = mkOption {
+          type = types.str;
+          example = "ldaps://ldap.host";
+          description = ''
+            LDAP host to connect against.
+          '';
+        };
+        bindDn = mkOption {
+          type = types.str;
+          example = "cn=root,dc=nixos,dc=org";
+          description = ''
+            Service account to bind against.
+          '';
+        };
+        bindPasswordFile = mkOption {
+          type = types.str;
+          example = "/run/secrets/ldap-bind";
+          description = ''
+            Path to the file containing the bind password of the servie account
+            defined by <xref linkend="opt-services.mailman.ldap.bindDn" />.
+          '';
+        };
+        superUserGroup = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "cn=admin,ou=groups,dc=nixos,dc=org";
+          description = ''
+            Group where a user must be a member of to gain superuser rights.
+          '';
+        };
+        userSearch = {
+          query = mkOption {
+            type = types.str;
+            example = "(&(objectClass=inetOrgPerson)(|(uid=%(user)s)(mail=%(user)s)))";
+            description = ''
+              Query to find a user in the LDAP database.
+            '';
+          };
+          ou = mkOption {
+            type = types.str;
+            example = "ou=users,dc=nixos,dc=org";
+            description = ''
+              Organizational unit to look up a user.
+            '';
+          };
+        };
+        groupSearch = {
+          type = mkOption {
+            type = types.enum [
+              "posixGroup" "groupOfNames" "memberDNGroup" "nestedMemberDNGroup" "nestedGroupOfNames"
+              "groupOfUniqueNames" "nestedGroupOfUniqueNames" "activeDirectoryGroup" "nestedActiveDirectoryGroup"
+              "organizationalRoleGroup" "nestedOrganizationalRoleGroup"
+            ];
+            default = "posixGroup";
+            apply = v: "${toUpper (substring 0 1 v)}${substring 1 (stringLength v) v}Type";
+            description = ''
+              Type of group to perform a group search against.
+            '';
+          };
+          query = mkOption {
+            type = types.str;
+            example = "(objectClass=groupOfNames)";
+            description = ''
+              Query to find a group associated to a user in the LDAP database.
+            '';
+          };
+          ou = mkOption {
+            type = types.str;
+            example = "ou=groups,dc=nixos,dc=org";
+            description = ''
+              Organizational unit to look up a group.
+            '';
+          };
+        };
+        attrMap = {
+          username = mkOption {
+            default = "uid";
+            type = types.str;
+            description = ''
+              LDAP-attribute that corresponds to the <literal>username</literal>-attribute in mailman.
+            '';
+          };
+          firstName = mkOption {
+            default = "givenName";
+            type = types.str;
+            description = ''
+              LDAP-attribute that corresponds to the <literal>firstName</literal>-attribute in mailman.
+            '';
+          };
+          lastName = mkOption {
+            default = "sn";
+            type = types.str;
+            description = ''
+              LDAP-attribute that corresponds to the <literal>lastName</literal>-attribute in mailman.
+            '';
+          };
+          email = mkOption {
+            default = "mail";
+            type = types.str;
+            description = ''
+              LDAP-attribute that corresponds to the <literal>email</literal>-attribute in mailman.
+            '';
+          };
+        };
       };
 
       enablePostfix = mkOption {
@@ -144,6 +253,14 @@ in {
         '';
       };
 
+      restApiPassFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          Path to the file containing the value for <literal>MAILMAN_REST_API_PASS</literal>.
+        '';
+      };
+
       serve = {
         enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web";
       };
@@ -185,14 +302,13 @@ in {
       mailman.layout = "fhs";
 
       "paths.fhs" = {
-        bin_dir = "${pkgs.python3Packages.mailman}/bin";
+        bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
         var_dir = "/var/lib/mailman";
         queue_dir = "$var_dir/queue";
         template_dir = "$var_dir/templates";
         log_dir = "/var/log/mailman";
         lock_dir = "$var_dir/lock";
         etc_dir = "/etc";
-        ext_dir = "$etc_dir/mailman.d";
         pid_file = "/run/mailman/master.pid";
       };
 
@@ -225,7 +341,14 @@ in {
               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
             '';
           };
-    in (lib.optionals cfg.enablePostfix [
+    in [
+      { assertion = cfg.webHosts != [];
+        message = ''
+          services.mailman.serve.enable requires there to be at least one entry
+          in services.mailman.webHosts.
+        '';
+      }
+    ] ++ (lib.optionals cfg.enablePostfix [
       { assertion = postfix.enable;
         message = ''
           Mailman's default NixOS configuration requires Postfix to be enabled.
@@ -254,8 +377,6 @@ in {
     };
     users.groups.mailman = {};
 
-    environment.etc."mailman.cfg".text = mailmanCfg;
-
     environment.etc."mailman3/settings.py".text = ''
       import os
 
@@ -273,26 +394,61 @@ in {
 
       with open('/var/lib/mailman-web/settings_local.json') as f:
           globals().update(json.load(f))
+
+      ${optionalString (cfg.restApiPassFile != null) ''
+        with open('${cfg.restApiPassFile}') as f:
+            MAILMAN_REST_API_PASS = f.read().rstrip('\n')
+      ''}
+
+      ${optionalString (cfg.ldap.enable) ''
+        import ldap
+        from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type}
+        AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}"
+        AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}"
+        with open("${cfg.ldap.bindPasswordFile}") as f:
+            AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n')
+        AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}",
+            ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}")
+        AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}()
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}",
+            ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}")
+        AUTH_LDAP_USER_ATTR_MAP = {
+          ${concatStrings (flip mapAttrsToList cfg.ldap.attrMap (key: value: ''
+            "${key}": "${value}",
+          ''))}
+        }
+        ${optionalString (cfg.ldap.superUserGroup != null) ''
+          AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+            "is_superuser": "${cfg.ldap.superUserGroup}"
+          }
+        ''}
+        AUTHENTICATION_BACKENDS = (
+            "django_auth_ldap.backend.LDAPBackend",
+            "django.contrib.auth.backends.ModelBackend"
+        )
+      ''}
     '';
 
-    services.nginx = mkIf cfg.serve.enable {
+    services.nginx = mkIf (cfg.serve.enable && cfg.webHosts != []) {
       enable = mkDefault true;
-      virtualHosts."${lib.head cfg.webHosts}" = {
-        serverAliases = cfg.webHosts;
+      virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
         locations = {
           "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
           "/static/".alias = webSettings.STATIC_ROOT + "/";
         };
-      };
+      });
     };
 
     environment.systemPackages = [ (pkgs.buildEnv {
       name = "mailman-tools";
       # We don't want to pollute the system PATH with a python
       # interpreter etc. so let's pick only the stuff we actually
-      # want from pythonEnv
+      # want from {web,mailman}Env
       pathsToLink = ["/bin"];
-      paths = [pythonEnv];
+      paths = [ mailmanEnv webEnv ];
+      # Only mailman-related stuff is installed, the rest is removed
+      # in `postBuild`.
+      ignoreCollisions = true;
       postBuild = ''
         find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
       '';
@@ -313,12 +469,16 @@ in {
     systemd.services = {
       mailman = {
         description = "GNU Mailman Master Process";
-        after = [ "network.target" ];
-        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        before = lib.optional cfg.enablePostfix "postfix.service";
+        after = [ "network.target" ]
+          ++ lib.optional cfg.enablePostfix "postfix-setup.service"
+          ++ lib.optional withPostgresql "postgresql.service";
+        restartTriggers = [ mailmanCfgFile ];
+        requires = optional withPostgresql "postgresql.service";
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman start";
-          ExecStop = "${pythonEnv}/bin/mailman stop";
+          ExecStart = "${mailmanEnv}/bin/mailman start";
+          ExecStop = "${mailmanEnv}/bin/mailman stop";
           User = "mailman";
           Group = "mailman";
           Type = "forking";
@@ -333,8 +493,18 @@ in {
         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         path = with pkgs; [ jq ];
+        after = optional withPostgresql "postgresql.service";
+        requires = optional withPostgresql "postgresql.service";
         serviceConfig.Type = "oneshot";
         script = ''
+          install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg
+          ${optionalString (cfg.restApiPassFile != null) ''
+            ${pkgs.replace-secret}/bin/replace-secret \
+              '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
+              ${cfg.restApiPassFile} \
+              /etc/mailman.cfg
+          ''}
+
           mailmanDir=/var/lib/mailman
           mailmanWebDir=/var/lib/mailman-web
 
@@ -374,9 +544,9 @@ in {
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         script = ''
           [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
-          ${pythonEnv}/bin/mailman-web migrate
-          ${pythonEnv}/bin/mailman-web collectstatic
-          ${pythonEnv}/bin/mailman-web compress
+          ${webEnv}/bin/mailman-web migrate
+          ${webEnv}/bin/mailman-web collectstatic
+          ${webEnv}/bin/mailman-web compress
         '';
         serviceConfig = {
           User = cfg.webUser;
@@ -390,14 +560,16 @@ in {
         uwsgiConfig.uwsgi = {
           type = "normal";
           plugins = ["python3"];
-          home = pythonEnv;
+          home = webEnv;
           module = "mailman_web.wsgi";
           http = "127.0.0.1:18507";
         };
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
         wantedBy = ["multi-user.target"];
-        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"];
+        after = optional withPostgresql "postgresql.service";
+        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
+          ++ optional withPostgresql "postgresql.service";
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
           # Since the mailman-web settings.py obstinately creates a logs
@@ -413,9 +585,9 @@ in {
       mailman-daily = {
         description = "Trigger daily Mailman events";
         startAt = "daily";
-        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        restartTriggers = [ mailmanCfgFile ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman digests --send";
+          ExecStart = "${mailmanEnv}/bin/mailman digests --send";
           User = "mailman";
           Group = "mailman";
         };
@@ -427,7 +599,7 @@ in {
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         wantedBy = [ "mailman.service" "multi-user.target" ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman-web qcluster";
+          ExecStart = "${webEnv}/bin/mailman-web qcluster";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
@@ -446,7 +618,7 @@ in {
         inherit startAt;
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman-web runjobs ${name}";
+          ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
@@ -455,7 +627,7 @@ in {
   };
 
   meta = {
-    maintainers = with lib.maintainers; [ lheckemann qyliss ];
+    maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
     doc = ./mailman.xml;
   };
 
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index 23d3574ae27c5..de00c87b95a5b 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -723,23 +723,11 @@ in
         { ${setgidGroup}.gid = config.ids.gids.postdrop;
         };
 
-      systemd.services.postfix =
-        { description = "Postfix mail server";
-
-          wantedBy = [ "multi-user.target" ];
-          after = [ "network.target" ];
-          path = [ pkgs.postfix ];
-
-          serviceConfig = {
-            Type = "forking";
-            Restart = "always";
-            PIDFile = "/var/lib/postfix/queue/pid/master.pid";
-            ExecStart = "${pkgs.postfix}/bin/postfix start";
-            ExecStop = "${pkgs.postfix}/bin/postfix stop";
-            ExecReload = "${pkgs.postfix}/bin/postfix reload";
-          };
-
-          preStart = ''
+      systemd.services.postfix-setup =
+        { description = "Setup for Postfix mail server";
+          serviceConfig.RemainAfterExit = true;
+          serviceConfig.Type = "oneshot";
+          script = ''
             # Backwards compatibility
             if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then
               mkdir -p /var/lib
@@ -777,6 +765,24 @@ in
           '';
         };
 
+      systemd.services.postfix =
+        { description = "Postfix mail server";
+
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" "postfix-setup.service" ];
+          requires = [ "postfix-setup.service" ];
+          path = [ pkgs.postfix ];
+
+          serviceConfig = {
+            Type = "forking";
+            Restart = "always";
+            PIDFile = "/var/lib/postfix/queue/pid/master.pid";
+            ExecStart = "${pkgs.postfix}/bin/postfix start";
+            ExecStop = "${pkgs.postfix}/bin/postfix stop";
+            ExecReload = "${pkgs.postfix}/bin/postfix reload";
+          };
+        };
+
       services.postfix.config = (mapAttrs (_: v: mkDefault v) {
         compatibility_level  = pkgs.postfix.version;
         mail_owner           = cfg.user;
diff --git a/nixos/modules/services/mail/postfixadmin.nix b/nixos/modules/services/mail/postfixadmin.nix
index f5c8efb3076c4..8adae3c1a0138 100644
--- a/nixos/modules/services/mail/postfixadmin.nix
+++ b/nixos/modules/services/mail/postfixadmin.nix
@@ -114,7 +114,7 @@ in
               location ~* \.php$ {
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_pass unix:${fpm.socket};
-                include ${pkgs.nginx}/conf/fastcgi_params;
+                include ${config.services.nginx.package}/conf/fastcgi_params;
                 include ${pkgs.nginx}/conf/fastcgi.conf;
               }
             '';
@@ -177,7 +177,7 @@ in
 
     services.phpfpm.pools.postfixadmin = {
       user = user;
-      phpPackage = pkgs.php74;
+      phpPackage = pkgs.php81;
       phpOptions = ''
         error_log = 'stderr'
         log_errors = on
diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix
new file mode 100644
index 0000000000000..0f9bc4ef226d2
--- /dev/null
+++ b/nixos/modules/services/mail/public-inbox.nix
@@ -0,0 +1,579 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.public-inbox;
+  stateDir = "/var/lib/public-inbox";
+
+  manref = name: vol: "<citerefentry><refentrytitle>${name}</refentrytitle><manvolnum>${toString vol}</manvolnum></citerefentry>";
+
+  gitIni = pkgs.formats.gitIni { listsAsDuplicateKeys = true; };
+  iniAtom = elemAt gitIni.type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped/*either*/.functor.wrapped 0;
+
+  useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
+                    cfg.settings.publicinboxwatch.spamcheck == "spamc";
+
+  publicInboxDaemonOptions = proto: defaultPort: {
+    args = mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = "Command-line arguments to pass to ${manref "public-inbox-${proto}d" 1}.";
+    };
+    port = mkOption {
+      type = with types; nullOr (either str port);
+      default = defaultPort;
+      description = ''
+        Listening port.
+        Beware that public-inbox uses well-known ports number to decide whether to enable TLS or not.
+        Set to null and use <code>systemd.sockets.public-inbox-${proto}d.listenStreams</code>
+        if you need a more advanced listening.
+      '';
+    };
+    cert = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "/path/to/fullchain.pem";
+      description = "Path to TLS certificate to use for connections to ${manref "public-inbox-${proto}d" 1}.";
+    };
+    key = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "/path/to/key.pem";
+      description = "Path to TLS key to use for connections to ${manref "public-inbox-${proto}d" 1}.";
+    };
+  };
+
+  serviceConfig = srv:
+    let proto = removeSuffix "d" srv;
+        needNetwork = builtins.hasAttr proto cfg && cfg.${proto}.port == null;
+    in {
+    serviceConfig = {
+      # Enable JIT-compiled C (via Inline::C)
+      Environment = [ "PERL_INLINE_DIRECTORY=/run/public-inbox-${srv}/perl-inline" ];
+      # NonBlocking is REQUIRED to avoid a race condition
+      # if running simultaneous services.
+      NonBlocking = true;
+      #LimitNOFILE = 30000;
+      User = config.users.users."public-inbox".name;
+      Group = config.users.groups."public-inbox".name;
+      RuntimeDirectory = [
+          "public-inbox-${srv}/perl-inline"
+        ];
+      RuntimeDirectoryMode = "700";
+      # This is for BindPaths= and BindReadOnlyPaths=
+      # to allow traversal of directories they create inside RootDirectory=
+      UMask = "0066";
+      StateDirectory = ["public-inbox"];
+      StateDirectoryMode = "0750";
+      WorkingDirectory = stateDir;
+      BindReadOnlyPaths = [
+          "/etc"
+          "/run/systemd"
+          "${config.i18n.glibcLocales}"
+        ] ++
+        mapAttrsToList (name: inbox: inbox.description) cfg.inboxes ++
+        # Without confinement the whole Nix store
+        # is made available to the service
+        optionals (!config.systemd.services."public-inbox-${srv}".confinement.enable) [
+          "${pkgs.dash}/bin/dash:/bin/sh"
+          builtins.storeDir
+        ];
+      # The following options are only for optimizing:
+      # systemd-analyze security public-inbox-'*'
+      AmbientCapabilities = "";
+      CapabilityBoundingSet = "";
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = "";
+      LockPersonality = true;
+      MemoryDenyWriteExecute = true;
+      NoNewPrivileges = true;
+      PrivateNetwork = mkDefault (!needNetwork);
+      ProcSubset = "pid";
+      ProtectClock = true;
+      ProtectHome = mkDefault true;
+      ProtectHostname = true;
+      ProtectKernelLogs = true;
+      ProtectProc = "invisible";
+      #ProtectSystem = "strict";
+      RemoveIPC = true;
+      RestrictAddressFamilies = [ "AF_UNIX" ] ++
+        optionals needNetwork [ "AF_INET" "AF_INET6" ];
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      SystemCallFilter = [
+        "@system-service"
+        "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources"
+        # Not removing @setuid and @privileged because Inline::C needs them.
+        # Not removing @timer because git upload-pack needs it.
+      ];
+      SystemCallArchitectures = "native";
+
+      # The following options are redundant when confinement is enabled
+      RootDirectory = "/var/empty";
+      TemporaryFileSystem = "/";
+      PrivateMounts = true;
+      MountAPIVFS = true;
+      PrivateDevices = true;
+      PrivateTmp = true;
+      PrivateUsers = true;
+      ProtectControlGroups = true;
+      ProtectKernelModules = true;
+      ProtectKernelTunables = true;
+    };
+    confinement = {
+      # Until we agree upon doing it directly here in NixOS
+      # https://github.com/NixOS/nixpkgs/pull/104457#issuecomment-1115768447
+      # let the user choose to enable the confinement with:
+      # systemd.services.public-inbox-httpd.confinement.enable = true;
+      # systemd.services.public-inbox-imapd.confinement.enable = true;
+      # systemd.services.public-inbox-init.confinement.enable = true;
+      # systemd.services.public-inbox-nntpd.confinement.enable = true;
+      #enable = true;
+      mode = "full-apivfs";
+      # Inline::C needs a /bin/sh, and dash is enough
+      binSh = "${pkgs.dash}/bin/dash";
+      packages = [
+          pkgs.iana-etc
+          (getLib pkgs.nss)
+          pkgs.tzdata
+        ];
+    };
+  };
+in
+
+{
+  options.services.public-inbox = {
+    enable = mkEnableOption "the public-inbox mail archiver";
+    package = mkOption {
+      type = types.package;
+      default = pkgs.public-inbox;
+      defaultText = literalExpression "pkgs.public-inbox";
+      description = "public-inbox package to use.";
+    };
+    path = mkOption {
+      type = with types; listOf package;
+      default = [];
+      example = literalExpression "with pkgs; [ spamassassin ]";
+      description = ''
+        Additional packages to place in the path of public-inbox-mda,
+        public-inbox-watch, etc.
+      '';
+    };
+    inboxes = mkOption {
+      description = ''
+        Inboxes to configure, where attribute names are inbox names.
+      '';
+      default = {};
+      type = types.attrsOf (types.submodule ({name, ...}: {
+        freeformType = types.attrsOf iniAtom;
+        options.inboxdir = mkOption {
+          type = types.str;
+          default = "${stateDir}/inboxes/${name}";
+          description = "The absolute path to the directory which hosts the public-inbox.";
+        };
+        options.address = mkOption {
+          type = with types; listOf str;
+          example = "example-discuss@example.org";
+          description = "The email addresses of the public-inbox.";
+        };
+        options.url = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "https://example.org/lists/example-discuss";
+          description = "URL where this inbox can be accessed over HTTP.";
+        };
+        options.description = mkOption {
+          type = types.str;
+          example = "user/dev discussion of public-inbox itself";
+          description = "User-visible description for the repository.";
+          apply = pkgs.writeText "public-inbox-description-${name}";
+        };
+        options.newsgroup = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "NNTP group name for the inbox.";
+        };
+        options.watch = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = "Paths for ${manref "public-inbox-watch" 1} to monitor for new mail.";
+          example = [ "maildir:/path/to/test.example.com.git" ];
+        };
+        options.watchheader = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "List-Id:<test@example.com>";
+          description = ''
+            If specified, ${manref "public-inbox-watch" 1} will only process
+            mail containing a matching header.
+          '';
+        };
+        options.coderepo = mkOption {
+          type = (types.listOf (types.enum (attrNames cfg.settings.coderepo))) // {
+            description = "list of coderepo names";
+          };
+          default = [];
+          description = "Nicknames of a 'coderepo' section associated with the inbox.";
+        };
+      }));
+    };
+    imap = {
+      enable = mkEnableOption "the public-inbox IMAP server";
+    } // publicInboxDaemonOptions "imap" 993;
+    http = {
+      enable = mkEnableOption "the public-inbox HTTP server";
+      mounts = mkOption {
+        type = with types; listOf str;
+        default = [ "/" ];
+        example = [ "/lists/archives" ];
+        description = ''
+          Root paths or URLs that public-inbox will be served on.
+          If domain parts are present, only requests to those
+          domains will be accepted.
+        '';
+      };
+      args = (publicInboxDaemonOptions "http" 80).args;
+      port = mkOption {
+        type = with types; nullOr (either str port);
+        default = 80;
+        example = "/run/public-inbox-httpd.sock";
+        description = ''
+          Listening port or systemd's ListenStream= entry
+          to be used as a reverse proxy, eg. in nginx:
+          <code>locations."/inbox".proxyPass = "http://unix:''${config.services.public-inbox.http.port}:/inbox";</code>
+          Set to null and use <code>systemd.sockets.public-inbox-httpd.listenStreams</code>
+          if you need a more advanced listening.
+        '';
+      };
+    };
+    mda = {
+      enable = mkEnableOption "the public-inbox Mail Delivery Agent";
+      args = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = "Command-line arguments to pass to ${manref "public-inbox-mda" 1}.";
+      };
+    };
+    postfix.enable = mkEnableOption "the integration into Postfix";
+    nntp = {
+      enable = mkEnableOption "the public-inbox NNTP server";
+    } // publicInboxDaemonOptions "nntp" 563;
+    spamAssassinRules = mkOption {
+      type = with types; nullOr path;
+      default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
+      defaultText = literalExpression "\${cfg.package.sa_config}/user/.spamassassin/user_prefs";
+      description = "SpamAssassin configuration specific to public-inbox.";
+    };
+    settings = mkOption {
+      description = ''
+        Settings for the <link xlink:href="https://public-inbox.org/public-inbox-config.html">public-inbox config file</link>.
+      '';
+      default = {};
+      type = types.submodule {
+        freeformType = gitIni.type;
+        options.publicinbox = mkOption {
+          default = {};
+          description = "public inboxes";
+          type = types.submodule {
+            freeformType = with types; /*inbox name*/attrsOf (/*inbox option name*/attrsOf /*inbox option value*/iniAtom);
+            options.css = mkOption {
+              type = with types; listOf str;
+              default = [];
+              description = "The local path name of a CSS file for the PSGI web interface.";
+            };
+            options.nntpserver = mkOption {
+              type = with types; listOf str;
+              default = [];
+              example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
+              description = "NNTP URLs to this public-inbox instance";
+            };
+            options.wwwlisting = mkOption {
+              type = with types; enum [ "all" "404" "match=domain" ];
+              default = "404";
+              description = ''
+                Controls which lists (if any) are listed for when the root
+                public-inbox URL is accessed over HTTP.
+              '';
+            };
+          };
+        };
+        options.publicinboxmda.spamcheck = mkOption {
+          type = with types; enum [ "spamc" "none" ];
+          default = "none";
+          description = ''
+            If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
+            using SpamAssassin.
+          '';
+        };
+        options.publicinboxwatch.spamcheck = mkOption {
+          type = with types; enum [ "spamc" "none" ];
+          default = "none";
+          description = ''
+            If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
+            using SpamAssassin.
+          '';
+        };
+        options.publicinboxwatch.watchspam = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "maildir:/path/to/spam";
+          description = ''
+            If set, mail in this maildir will be trained as spam and
+            deleted from all watched inboxes
+          '';
+        };
+        options.coderepo = mkOption {
+          default = {};
+          description = "code repositories";
+          type = types.attrsOf (types.submodule {
+            freeformType = types.attrsOf iniAtom;
+            options.cgitUrl = mkOption {
+              type = types.str;
+              description = "URL of a cgit instance";
+            };
+            options.dir = mkOption {
+              type = types.str;
+              description = "Path to a git repository";
+            };
+          });
+        };
+      };
+    };
+    openFirewall = mkEnableOption "opening the firewall when using a port option";
+  };
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = config.services.spamassassin.enable || !useSpamAssassin;
+        message = ''
+          public-inbox is configured to use SpamAssassin, but
+          services.spamassassin.enable is false.  If you don't need
+          spam checking, set `services.public-inbox.settings.publicinboxmda.spamcheck' and
+          `services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
+        '';
+      }
+      { assertion = cfg.path != [] || !useSpamAssassin;
+        message = ''
+          public-inbox is configured to use SpamAssassin, but there is
+          no spamc executable in services.public-inbox.path.  If you
+          don't need spam checking, set
+          `services.public-inbox.settings.publicinboxmda.spamcheck' and
+          `services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
+        '';
+      }
+    ];
+    services.public-inbox.settings =
+      filterAttrsRecursive (n: v: v != null) {
+        publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
+    };
+    users = {
+      users.public-inbox = {
+        home = stateDir;
+        group = "public-inbox";
+        isSystemUser = true;
+      };
+      groups.public-inbox = {};
+    };
+    networking.firewall = mkIf cfg.openFirewall
+      { allowedTCPPorts = mkMerge
+        (map (proto: (mkIf (cfg.${proto}.enable && types.port.check cfg.${proto}.port) [ cfg.${proto}.port ]))
+        ["imap" "http" "nntp"]);
+      };
+    services.postfix = mkIf (cfg.postfix.enable && cfg.mda.enable) {
+      # Not sure limiting to 1 is necessary, but better safe than sorry.
+      config.public-inbox_destination_recipient_limit = "1";
+
+      # Register the addresses as existing
+      virtual =
+        concatStringsSep "\n" (mapAttrsToList (_: inbox:
+          concatMapStringsSep "\n" (address:
+            "${address} ${address}"
+          ) inbox.address
+        ) cfg.inboxes);
+
+      # Deliver the addresses with the public-inbox transport
+      transport =
+        concatStringsSep "\n" (mapAttrsToList (_: inbox:
+          concatMapStringsSep "\n" (address:
+            "${address} public-inbox:${address}"
+          ) inbox.address
+        ) cfg.inboxes);
+
+      # The public-inbox transport
+      masterConfig.public-inbox = {
+        type = "unix";
+        privileged = true; # Required for user=
+        command = "pipe";
+        args = [
+          "flags=X" # Report as a final delivery
+          "user=${with config.users; users."public-inbox".name + ":" + groups."public-inbox".name}"
+          # Specifying a nexthop when using the transport
+          # (eg. test public-inbox:test) allows to
+          # receive mails with an extension (eg. test+foo).
+          "argv=${pkgs.writeShellScript "public-inbox-transport" ''
+            export HOME="${stateDir}"
+            export ORIGINAL_RECIPIENT="''${2:-1}"
+            export PATH="${makeBinPath cfg.path}:$PATH"
+            exec ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
+          ''} \${original_recipient} \${nexthop}"
+        ];
+      };
+    };
+    systemd.sockets = mkMerge (map (proto:
+      mkIf (cfg.${proto}.enable && cfg.${proto}.port != null)
+        { "public-inbox-${proto}d" = {
+            listenStreams = [ (toString cfg.${proto}.port) ];
+            wantedBy = [ "sockets.target" ];
+          };
+        }
+      ) [ "imap" "http" "nntp" ]);
+    systemd.services = mkMerge [
+      (mkIf cfg.imap.enable
+        { public-inbox-imapd = mkMerge [(serviceConfig "imapd") {
+          after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+          requires = [ "public-inbox-init.service" ];
+          serviceConfig = {
+            ExecStart = escapeShellArgs (
+              [ "${cfg.package}/bin/public-inbox-imapd" ] ++
+              cfg.imap.args ++
+              optionals (cfg.imap.cert != null) [ "--cert" cfg.imap.cert ] ++
+              optionals (cfg.imap.key != null) [ "--key" cfg.imap.key ]
+            );
+          };
+        }];
+      })
+      (mkIf cfg.http.enable
+        { public-inbox-httpd = mkMerge [(serviceConfig "httpd") {
+          after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+          requires = [ "public-inbox-init.service" ];
+          serviceConfig = {
+            ExecStart = escapeShellArgs (
+              [ "${cfg.package}/bin/public-inbox-httpd" ] ++
+              cfg.http.args ++
+              # See https://public-inbox.org/public-inbox.git/tree/examples/public-inbox.psgi
+              # for upstream's example.
+              [ (pkgs.writeText "public-inbox.psgi" ''
+                #!${cfg.package.fullperl} -w
+                use strict;
+                use warnings;
+                use Plack::Builder;
+                use PublicInbox::WWW;
+
+                my $www = PublicInbox::WWW->new;
+                $www->preload;
+
+                builder {
+                  # If reached through a reverse proxy,
+                  # make it transparent by resetting some HTTP headers
+                  # used by public-inbox to generate URIs.
+                  enable 'ReverseProxy';
+
+                  # No need to send a response body if it's an HTTP HEAD requests.
+                  enable 'Head';
+
+                  # Route according to configured domains and root paths.
+                  ${concatMapStrings (path: ''
+                  mount q(${path}) => sub { $www->call(@_); };
+                  '') cfg.http.mounts}
+                }
+              '') ]
+            );
+          };
+        }];
+      })
+      (mkIf cfg.nntp.enable
+        { public-inbox-nntpd = mkMerge [(serviceConfig "nntpd") {
+          after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+          requires = [ "public-inbox-init.service" ];
+          serviceConfig = {
+            ExecStart = escapeShellArgs (
+              [ "${cfg.package}/bin/public-inbox-nntpd" ] ++
+              cfg.nntp.args ++
+              optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++
+              optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ]
+            );
+          };
+        }];
+      })
+      (mkIf (any (inbox: inbox.watch != []) (attrValues cfg.inboxes)
+        || cfg.settings.publicinboxwatch.watchspam != null)
+        { public-inbox-watch = mkMerge [(serviceConfig "watch") {
+          inherit (cfg) path;
+          wants = [ "public-inbox-init.service" ];
+          requires = [ "public-inbox-init.service" ] ++
+            optional (cfg.settings.publicinboxwatch.spamcheck == "spamc") "spamassassin.service";
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            ExecStart = "${cfg.package}/bin/public-inbox-watch";
+            ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          };
+        }];
+      })
+      ({ public-inbox-init = let
+          PI_CONFIG = gitIni.generate "public-inbox.ini"
+            (filterAttrsRecursive (n: v: v != null) cfg.settings);
+          in mkMerge [(serviceConfig "init") {
+          wantedBy = [ "multi-user.target" ];
+          restartIfChanged = true;
+          restartTriggers = [ PI_CONFIG ];
+          script = ''
+            set -ux
+            install -D -p ${PI_CONFIG} ${stateDir}/.public-inbox/config
+            '' + optionalString useSpamAssassin ''
+              install -m 0700 -o spamd -d ${stateDir}/.spamassassin
+              ${optionalString (cfg.spamAssassinRules != null) ''
+                ln -sf ${cfg.spamAssassinRules} ${stateDir}/.spamassassin/user_prefs
+              ''}
+            '' + concatStrings (mapAttrsToList (name: inbox: ''
+              if [ ! -e ${stateDir}/inboxes/${escapeShellArg name} ]; then
+                # public-inbox-init creates an inbox and adds it to a config file.
+                # It tries to atomically write the config file by creating
+                # another file in the same directory, and renaming it.
+                # This has the sad consequence that we can't use
+                # /dev/null, or it would try to create a file in /dev.
+                conf_dir="$(mktemp -d)"
+
+                PI_CONFIG=$conf_dir/conf \
+                ${cfg.package}/bin/public-inbox-init -V2 \
+                  ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" inbox.url ] ++ inbox.address)}
+
+                rm -rf $conf_dir
+              fi
+
+              ln -sf ${inbox.description} \
+                ${stateDir}/inboxes/${escapeShellArg name}/description
+
+              export GIT_DIR=${stateDir}/inboxes/${escapeShellArg name}/all.git
+              if test -d "$GIT_DIR"; then
+                # Config is inherited by each epoch repository,
+                # so just needs to be set for all.git.
+                ${pkgs.git}/bin/git config core.sharedRepository 0640
+              fi
+            '') cfg.inboxes
+            ) + ''
+            shopt -s nullglob
+            for inbox in ${stateDir}/inboxes/*/; do
+              # This should be idempotent, but only do it for new
+              # inboxes anyway because it's only needed once, and could
+              # be slow for large pre-existing inboxes.
+              ls -1 "$inbox" | grep -q '^xap' ||
+              ${cfg.package}/bin/public-inbox-index "$inbox"
+            done
+          '';
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            StateDirectory = [
+              "public-inbox/.public-inbox"
+              "public-inbox/.public-inbox/emergency"
+              "public-inbox/inboxes"
+            ];
+          };
+        }];
+      })
+    ];
+    environment.systemPackages = with pkgs; [ cfg.package ];
+  };
+  meta.maintainers = with lib.maintainers; [ julm qyliss ];
+}
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index ac192c56aa604..1dd393da88221 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -153,7 +153,7 @@ in
               location ~* \.php$ {
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_pass unix:${fpm.socket};
-                include ${pkgs.nginx}/conf/fastcgi_params;
+                include ${config.services.nginx.package}/conf/fastcgi_params;
                 include ${pkgs.nginx}/conf/fastcgi.conf;
               }
             '';
diff --git a/nixos/modules/services/mail/schleuder.nix b/nixos/modules/services/mail/schleuder.nix
new file mode 100644
index 0000000000000..7ba15f1070bde
--- /dev/null
+++ b/nixos/modules/services/mail/schleuder.nix
@@ -0,0 +1,162 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.schleuder;
+  settingsFormat = pkgs.formats.yaml { };
+  postfixMap = entries: lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${value}") entries);
+  writePostfixMap = name: entries: pkgs.writeText name (postfixMap entries);
+  configScript = pkgs.writeScript "schleuder-cfg" ''
+    #!${pkgs.runtimeShell}
+    set -exuo pipefail
+    umask 0077
+    ${pkgs.yq}/bin/yq \
+      --slurpfile overrides <(${pkgs.yq}/bin/yq . <${lib.escapeShellArg cfg.extraSettingsFile}) \
+      < ${settingsFormat.generate "schleuder.yml" cfg.settings} \
+      '. * $overrides[0]' \
+      > /etc/schleuder/schleuder.yml
+    chown schleuder: /etc/schleuder/schleuder.yml
+  '';
+in
+{
+  options.services.schleuder = {
+    enable = lib.mkEnableOption "Schleuder secure remailer";
+    enablePostfix = lib.mkEnableOption "automatic postfix integration" // { default = true; };
+    lists = lib.mkOption {
+      description = ''
+        List of list addresses that should be handled by Schleuder.
+
+        Note that this is only handled by the postfix integration, and
+        the setup of the lists, their members and their keys has to be
+        performed separately via schleuder's API, using a tool such as
+        schleuder-cli.
+      '';
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "widget-team@example.com" "security@example.com" ];
+    };
+    /* maybe one day....
+      domains = lib.mkOption {
+      description = "Domains for which all mail should be handled by Schleuder.";
+      type = lib.types.listOf lib.types.str;
+      default = [];
+      example = ["securelists.example.com"];
+      };
+    */
+    settings = lib.mkOption {
+      description = ''
+        Settings for schleuder.yml.
+
+        Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/blob/master/etc/schleuder.yml">example configuration</link> for possible values.
+      '';
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options.keyserver = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            Key server from which to fetch and update keys.
+
+            Note that NixOS uses a different default from upstream, since the upstream default sks-keyservers.net is deprecated.
+          '';
+          default = "keys.openpgp.org";
+        };
+      };
+      default = { };
+    };
+    extraSettingsFile = lib.mkOption {
+      description = "YAML file to merge into the schleuder config at runtime. This can be used for secrets such as API keys.";
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+    };
+    listDefaults = lib.mkOption {
+      description = ''
+        Default settings for lists (list-defaults.yml).
+
+        Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/-/blob/master/etc/list-defaults.yml">example configuration</link> for possible values.
+      '';
+      type = settingsFormat.type;
+      default = { };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(cfg.settings.api ? valid_api_keys);
+        message = ''
+          services.schleuder.settings.api.valid_api_keys is set. Defining API keys via NixOS config results in them being copied to the world-readable Nix store. Please use the extraSettingsFile option to store API keys in a non-public location.
+        '';
+      }
+      {
+        assertion = !(lib.any (db: db ? password) (lib.attrValues cfg.settings.database or {}));
+        message = ''
+          A password is defined for at least one database in services.schleuder.settings.database. Defining passwords via NixOS config results in them being copied to the world-readable Nix store. Please use the extraSettingsFile option to store database passwords in a non-public location.
+        '';
+      }
+    ];
+    users.users.schleuder.isSystemUser = true;
+    users.users.schleuder.group = "schleuder";
+    users.groups.schleuder = {};
+    environment.systemPackages = [
+      pkgs.schleuder-cli
+    ];
+    services.postfix = lib.mkIf cfg.enablePostfix {
+      extraMasterConf = ''
+        schleuder  unix  -       n       n       -       -       pipe
+          flags=DRhu user=schleuder argv=/${pkgs.schleuder}/bin/schleuder work ''${recipient}
+      '';
+      transport = lib.mkIf (cfg.lists != [ ]) (postfixMap (lib.genAttrs cfg.lists (_: "schleuder:")));
+      extraConfig = ''
+        schleuder_destination_recipient_limit = 1
+      '';
+      # review: does this make sense?
+      localRecipients = lib.mkIf (cfg.lists != [ ]) cfg.lists;
+    };
+    systemd.services = let commonServiceConfig = {
+      # We would have liked to use DynamicUser, but since the default
+      # database is SQLite and lives in StateDirectory, and that same
+      # database needs to be readable from the postfix service, this
+      # isn't trivial to do.
+      User = "schleuder";
+      StateDirectory = "schleuder";
+      StateDirectoryMode = "0700";
+    }; in
+      {
+        schleuder-init = {
+          serviceConfig = commonServiceConfig // {
+            ExecStartPre = lib.mkIf (cfg.extraSettingsFile != null) [
+              "+${configScript}"
+            ];
+            ExecStart = [ "${pkgs.schleuder}/bin/schleuder install" ];
+            Type = "oneshot";
+          };
+        };
+        schleuder-api-daemon = {
+          after = [ "local-fs.target" "network.target" "schleuder-init.service" ];
+          wantedBy = [ "multi-user.target" ];
+          requires = [ "schleuder-init.service" ];
+          serviceConfig = commonServiceConfig // {
+            ExecStart = [ "${pkgs.schleuder}/bin/schleuder-api-daemon" ];
+          };
+        };
+        schleuder-weekly-key-maintenance = {
+          after = [ "local-fs.target" "network.target" ];
+          startAt = "weekly";
+          serviceConfig = commonServiceConfig // {
+            ExecStart = [
+              "${pkgs.schleuder}/bin/schleuder refresh_keys"
+              "${pkgs.schleuder}/bin/schleuder check_keys"
+            ];
+          };
+        };
+      };
+
+    environment.etc."schleuder/schleuder.yml" = lib.mkIf (cfg.extraSettingsFile == null) {
+      source = settingsFormat.generate "schleuder.yml" cfg.settings;
+    };
+    environment.etc."schleuder/list-defaults.yml".source = settingsFormat.generate "list-defaults.yml" cfg.listDefaults;
+
+    services.schleuder = {
+      #lists_dir = "/var/lib/schleuder.lists";
+      settings.filters_dir = lib.mkDefault "/var/lib/schleuder/filters";
+      settings.keyword_handlers_dir = lib.mkDefault "/var/lib/schleuder/keyword_handlers";
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index ac878222b26a2..3b10d8d29090d 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -135,7 +135,7 @@ in
         User = "spamd";
         Group = "spamd";
         StateDirectory = "spamassassin";
-        ExecStartPost = "+${pkgs.systemd}/bin/systemctl -q --no-block try-reload-or-restart spamd.service";
+        ExecStartPost = "+${config.systemd.package}/bin/systemctl -q --no-block try-reload-or-restart spamd.service";
       };
 
       script = ''
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/matrix/appservice-discord.nix
index 8a8c7f41e3cbb..8a8c7f41e3cbb 100644
--- a/nixos/modules/services/misc/matrix-appservice-discord.nix
+++ b/nixos/modules/services/matrix/appservice-discord.nix
diff --git a/nixos/modules/services/misc/matrix-appservice-irc.nix b/nixos/modules/services/matrix/appservice-irc.nix
index b041c9c82c56e..ff938527ed58a 100644
--- a/nixos/modules/services/misc/matrix-appservice-irc.nix
+++ b/nixos/modules/services/matrix/appservice-irc.nix
@@ -153,6 +153,9 @@ in {
     systemd.services.matrix-appservice-irc = {
       description = "Matrix-IRC bridge";
       before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
+      after = lib.optionals (cfg.settings.database.engine == "postgres") [
+        "postgresql.service"
+      ];
       wantedBy = [ "multi-user.target" ];
 
       preStart = ''
diff --git a/nixos/modules/services/misc/matrix-conduit.nix b/nixos/modules/services/matrix/conduit.nix
index 108f64de7aa95..108f64de7aa95 100644
--- a/nixos/modules/services/misc/matrix-conduit.nix
+++ b/nixos/modules/services/matrix/conduit.nix
diff --git a/nixos/modules/services/misc/dendrite.nix b/nixos/modules/services/matrix/dendrite.nix
index c967fc3a362a7..54052084b3378 100644
--- a/nixos/modules/services/misc/dendrite.nix
+++ b/nixos/modules/services/matrix/dendrite.nix
@@ -74,6 +74,18 @@ in
         <literal>dendrite</literal> is running.
       '';
     };
+    loadCredential = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "private_key:/path/to/my_private_key" ];
+      description = ''
+        This can be used to pass secrets to the systemd service without adding them to
+        the nix store.
+        To use the example setting, see the example of
+        <option>services.dendrite.settings.global.private_key</option>.
+        See the LoadCredential section of systemd.exec manual for more information.
+      '';
+    };
     settings = lib.mkOption {
       type = lib.types.submodule {
         freeformType = settingsFormat.type;
@@ -88,8 +100,10 @@ in
             '';
           };
           private_key = lib.mkOption {
-            type = lib.types.path;
-            example = "${workingDir}/matrix_key.pem";
+            type = lib.types.either
+              lib.types.path
+              (lib.types.strMatching "^\\$CREDENTIALS_DIRECTORY/.+");
+            example = "$CREDENTIALS_DIRECTORY/private_key";
             description = ''
               The path to the signing private key file, used to sign
               requests and events.
@@ -110,6 +124,15 @@ in
             '';
           };
         };
+        options.app_service_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Appservice API.
+            '';
+          };
+        };
         options.client_api = {
           registration_disabled = lib.mkOption {
             type = lib.types.bool;
@@ -120,6 +143,91 @@ in
             '';
           };
         };
+        options.federation_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Federation API.
+            '';
+          };
+        };
+        options.key_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:keyserver.db";
+            description = ''
+              Database for the Key Server (for end-to-end encryption).
+            '';
+          };
+        };
+        options.media_api = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mediaapi.db";
+              description = ''
+                Database for the Media API.
+              '';
+            };
+          };
+          base_path = lib.mkOption {
+            type = lib.types.str;
+            default = "${workingDir}/media_store";
+            description = ''
+              Storage path for uploaded media.
+            '';
+          };
+        };
+        options.room_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:roomserver.db";
+            description = ''
+              Database for the Room Server.
+            '';
+          };
+        };
+        options.sync_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:syncserver.db";
+            description = ''
+              Database for the Sync API.
+            '';
+          };
+        };
+        options.user_api = {
+          account_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_accounts.db";
+              description = ''
+                Database for the User API, accounts.
+              '';
+            };
+          };
+          device_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_devices.db";
+              description = ''
+                Database for the User API, devices.
+              '';
+            };
+          };
+        };
+        options.mscs = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mscs.db";
+              description = ''
+                Database for exerimental MSC's.
+              '';
+            };
+          };
+        };
       };
       default = { };
       description = ''
@@ -128,6 +236,13 @@ in
         for available options with which to populate settings.
       '';
     };
+    openRegistration = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Allow open registration without secondary verification (reCAPTCHA).
+      '';
+    };
   };
 
   config = lib.mkIf cfg.enable {
@@ -153,15 +268,14 @@ in
         WorkingDirectory = workingDir;
         RuntimeDirectory = "dendrite";
         RuntimeDirectoryMode = "0700";
+        LimitNOFILE = 65535;
         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
-        ExecStartPre =
-          if (cfg.environmentFile != null) then ''
-            ${pkgs.envsubst}/bin/envsubst \
-              -i ${configurationYaml} \
-              -o /run/dendrite/dendrite.yaml
-          '' else ''
-            ${pkgs.coreutils}/bin/cp ${configurationYaml} /run/dendrite/dendrite.yaml
-          '';
+        LoadCredential = cfg.loadCredential;
+        ExecStartPre = ''
+          ${pkgs.envsubst}/bin/envsubst \
+            -i ${configurationYaml} \
+            -o /run/dendrite/dendrite.yaml
+        '';
         ExecStart = lib.strings.concatStringsSep " " ([
           "${pkgs.dendrite}/bin/dendrite-monolith-server"
           "--config /run/dendrite/dendrite.yaml"
@@ -171,6 +285,8 @@ in
           "--https-bind-address :${builtins.toString cfg.httpsPort}"
           "--tls-cert ${cfg.tlsCert}"
           "--tls-key ${cfg.tlsKey}"
+        ] ++ lib.optionals cfg.openRegistration [
+          "--really-enable-open-registration"
         ]);
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
diff --git a/nixos/modules/services/misc/mautrix-facebook.nix b/nixos/modules/services/matrix/mautrix-facebook.nix
index e046c791ac013..e046c791ac013 100644
--- a/nixos/modules/services/misc/mautrix-facebook.nix
+++ b/nixos/modules/services/matrix/mautrix-facebook.nix
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix
index 794c4dd9ddcd7..794c4dd9ddcd7 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/matrix/mautrix-telegram.nix
diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml
index d462ddf7b01be..b07abe3397917 100644
--- a/nixos/modules/services/matrix/mjolnir.xml
+++ b/nixos/modules/services/matrix/mjolnir.xml
@@ -98,7 +98,7 @@
   </para>
   <para>
    To use the Antispam Module, add <package>matrix-synapse-plugins.matrix-synapse-mjolnir-antispam</package>
-   to the Synapse plugin list and enable the <literal>mjolnir.AntiSpam</literal> module.
+   to the Synapse plugin list and enable the <literal>mjolnir.Module</literal> module.
   </para>
 <programlisting>
 {
@@ -108,7 +108,7 @@
     ];
     extraConfig = ''
       modules:
-        - module: mjolnir.AntiSpam
+        - module: mjolnir.Module
           config:
             # Prevent servers/users in the ban lists from inviting users on this
             # server to rooms. Default true.
diff --git a/nixos/modules/services/misc/matrix-synapse-log_config.yaml b/nixos/modules/services/matrix/synapse-log_config.yaml
index d85bdd1208f9a..d85bdd1208f9a 100644
--- a/nixos/modules/services/misc/matrix-synapse-log_config.yaml
+++ b/nixos/modules/services/matrix/synapse-log_config.yaml
diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix
new file mode 100644
index 0000000000000..3d5d10cdf070b
--- /dev/null
+++ b/nixos/modules/services/matrix/synapse.nix
@@ -0,0 +1,774 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-synapse;
+  format = pkgs.formats.yaml {};
+
+  # remove null values from the final configuration
+  finalSettings = lib.filterAttrsRecursive (_: v: v != null) cfg.settings;
+  configFile = format.generate "homeserver.yaml" finalSettings;
+  logConfigFile = format.generate "log_config.yaml" cfg.logConfig;
+
+  pluginsEnv = cfg.package.python.buildEnv.override {
+    extraLibs = cfg.plugins;
+  };
+
+  usePostgresql = cfg.settings.database.name == "psycopg2";
+  hasLocalPostgresDB = let args = cfg.settings.database.args; in
+    usePostgresql && (!(args ? host) || (elem args.host [ "localhost" "127.0.0.1" "::1" ]));
+
+  registerNewMatrixUser =
+    let
+      isIpv6 = x: lib.length (lib.splitString ":" x) > 1;
+      listener =
+        lib.findFirst (
+          listener: lib.any (
+            resource: lib.any (
+              name: name == "client"
+            ) resource.names
+          ) listener.resources
+        ) (lib.last cfg.settings.listeners) cfg.settings.listeners;
+        # FIXME: Handle cases with missing client listener properly,
+        # don't rely on lib.last, this will not work.
+
+      # add a tail, so that without any bind_addresses we still have a useable address
+      bindAddress = head (listener.bind_addresses ++ [ "127.0.0.1" ]);
+      listenerProtocol = if listener.tls
+        then "https"
+        else "http";
+    in
+    pkgs.writeShellScriptBin "matrix-synapse-register_new_matrix_user" ''
+      exec ${cfg.package}/bin/register_new_matrix_user \
+        $@ \
+        ${lib.concatMapStringsSep " " (x: "-c ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} \
+        "${listenerProtocol}://${
+          if (isIpv6 bindAddress) then
+            "[${bindAddress}]"
+          else
+            "${bindAddress}"
+        }:${builtins.toString listener.port}/"
+    '';
+in {
+
+  imports = [
+
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "trusted_third_party_id_servers" ] ''
+      The `trusted_third_party_id_servers` option as been removed in `matrix-synapse` v1.4.0
+      as the behavior is now obsolete.
+    '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] ''
+      Database configuration must be done manually. An exemplary setup is demonstrated in
+      <nixpkgs/nixos/tests/matrix-synapse.nix>
+    '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
+      You may add additional event types via
+      `services.matrix-synapse.room_prejoin_state.additional_event_types` and
+      disable the default events via
+      `services.matrix-synapse.room_prejoin_state.disable_default_event_types`.
+    '')
+
+    # options that don't exist in synapse anymore
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_host" ] "Use listener settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_port" ] "Use listener settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "expire_access_tokens" ] "" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "no_tls" ] "It is no longer supported by synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_dh_param_path" ] "It was removed from synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "unsecure_port" ] "Use settings.listeners instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "user_creation_max_duration" ] "It is no longer supported by synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "verbose" ] "Use a log config instead." )
+
+    # options that were moved into rfc42 style settigns
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "app_service_config_files" ] "Use settings.app_service_config_files instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_args" ] "Use settings.database.args instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_name" ] "Use settings.database.args.database instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_type" ] "Use settings.database.name instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_user" ] "Use settings.database.args.user instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "dynamic_thumbnails" ] "Use settings.dynamic_thumbnails instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_metrics" ] "Use settings.enable_metrics instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration" ] "Use settings.enable_registration instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "extraConfig" ] "Use settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "listeners" ] "Use settings.listeners instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "logConfig" ] "Use settings.log_config instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "max_image_pixels" ] "Use settings.max_image_pixels instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "max_upload_size" ] "Use settings.max_upload_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "presence" "enabled" ] "Use settings.presence.enabled instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "public_baseurl" ] "Use settings.public_baseurl instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "report_stats" ] "Use settings.report_stats instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "server_name" ] "Use settings.server_name instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "servers" ] "Use settings.trusted_key_servers instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_certificate_path" ] "Use settings.tls_certificate_path instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_private_key_path" ] "Use settings.tls_private_key_path instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_shared_secret" ] "Use settings.turn_shared_secret instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_uris" ] "Use settings.turn_uris instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_user_lifetime" ] "Use settings.turn_user_lifetime instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_enabled" ] "Use settings.url_preview_enabled instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_blacklist" ] "Use settings.url_preview_ip_range_blacklist instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_whitelist" ] "Use settings.url_preview_ip_range_whitelist instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_url_blacklist" ] "Use settings.url_preview_url_blacklist instead" )
+
+    # options that are too specific to mention them explicitly in settings
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "email" ] "Use settings.account_threepid_delegates.email instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "msisdn" ] "Use settings.account_threepid_delegates.msisdn instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "allow_guest_access" ] "Use settings.allow_guest_access instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bcrypt_rounds" ] "Use settings.bcrypt_rounds instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration_captcha" ] "Use settings.enable_registration_captcha instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "event_cache_size" ] "Use settings.event_cache_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_concurrent" ] "Use settings.rc_federation.concurrent instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_reject_limit" ] "Use settings.rc_federation.reject_limit instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_delay" ] "Use settings.rc_federation.sleep_delay instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_limit" ] "Use settings.rc_federation.sleep_limit instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_window_size" ] "Use settings.rc_federation.window_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "key_refresh_interval" ] "Use settings.key_refresh_interval instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_burst_count" ] "Use settings.rc_messages.burst_count instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_per_second" ] "Use settings.rc_messages.per_second instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_private_key" ] "Use settings.recaptcha_private_key instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_public_key" ] "Use settings.recaptcha_public_key instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "redaction_retention_period" ] "Use settings.redaction_retention_period instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "additional_event_types" ] "Use settings.room_prejoin_state.additional_event_types instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "disable_default_event_types" ] "Use settings.room_prejoin-state.disable_default_event_types instead" )
+
+    # Options that should be passed via extraConfigFiles, so they are not persisted into the nix store
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "macaroon_secret_key" ] "Pass this value via extraConfigFiles instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "registration_shared_secret" ] "Pass this value via extraConfigFiles instead" )
+
+  ];
+
+  options = {
+    services.matrix-synapse = {
+      enable = mkEnableOption "matrix.org synapse";
+
+      configFile = mkOption {
+        type = types.path;
+        readOnly = true;
+        description = ''
+          Path to the configuration file on the target system. Useful to configure e.g. workers
+          that also need this.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-synapse;
+        defaultText = literalExpression "pkgs.matrix-synapse";
+        description = ''
+          Overridable attribute of the matrix synapse server package to use.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExpression ''
+          with config.services.matrix-synapse.package.plugins; [
+            matrix-synapse-ldap3
+            matrix-synapse-pam
+          ];
+        '';
+        description = ''
+          List of additional Matrix plugins to make available.
+        '';
+      };
+
+      withJemalloc = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to preload jemalloc to reduce memory fragmentation and overall usage.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/matrix-synapse";
+        description = ''
+          The directory where matrix-synapse stores its stateful data such as
+          certificates, media and uploads.
+        '';
+      };
+
+      settings = mkOption {
+        default = {};
+        description = mdDoc ''
+          The primary synapse configuration. See the
+          [sample configuration](https://github.com/matrix-org/synapse/blob/v${cfg.package.version}/docs/sample_config.yaml)
+          for possible values.
+
+          Secrets should be passed in by using the `extraConfigFiles` option.
+        '';
+        type = with types; submodule {
+          freeformType = format.type;
+          options = {
+            # This is a reduced set of popular options and defaults
+            # Do not add every available option here, they can be specified
+            # by the user at their own discretion. This is a freeform type!
+
+            server_name = mkOption {
+              type = types.str;
+              example = "example.com";
+              default = config.networking.hostName;
+              defaultText = literalExpression "config.networking.hostName";
+              description = ''
+                The domain name of the server, with optional explicit port.
+                This is used by remote servers to look up the server address.
+                This is also the last part of your UserID.
+
+                The server_name cannot be changed later so it is important to configure this correctly before you start Synapse.
+              '';
+            };
+
+            enable_registration = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Enable registration for new users.
+              '';
+            };
+
+            registration_shared_secret = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = mdDoc ''
+                If set, allows registration by anyone who also has the shared
+                secret, even if registration is otherwise disabled.
+
+                Secrets should be passed in via `extraConfigFiles`!
+              '';
+            };
+
+            macaroon_secret_key = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = mdDoc ''
+                Secret key for authentication tokens. If none is specified,
+                the registration_shared_secret is used, if one is given; otherwise,
+                a secret key is derived from the signing key.
+
+                Secrets should be passed in via `extraConfigFiles`!
+              '';
+            };
+
+            enable_metrics = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Enable collection and rendering of performance metrics
+              '';
+            };
+
+            report_stats = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether or not to report anonymized homeserver usage statistics.
+              '';
+            };
+
+            signing_key_path = mkOption {
+              type = types.path;
+              default = "${cfg.dataDir}/homeserver.signing.key";
+              description = ''
+                Path to the signing key to sign messages with.
+              '';
+            };
+
+            pid_file = mkOption {
+              type = types.path;
+              default = "/run/matrix-synapse.pid";
+              readOnly = true;
+              description = ''
+                The file to store the PID in.
+              '';
+            };
+
+            log_config = mkOption {
+              type = types.path;
+              default = ./synapse-log_config.yaml;
+              description = ''
+                The file that holds the logging configuration.
+              '';
+            };
+
+            media_store_path = mkOption {
+              type = types.path;
+              default = if lib.versionAtLeast config.system.stateVersion "22.05"
+                then "${cfg.dataDir}/media_store"
+                else "${cfg.dataDir}/media";
+              defaultText = "${cfg.dataDir}/media_store for when system.stateVersion is at least 22.05, ${cfg.dataDir}/media when lower than 22.05";
+              description = ''
+                Directory where uploaded images and attachments are stored.
+              '';
+            };
+
+            public_baseurl = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "https://example.com:8448/";
+              description = ''
+                The public-facing base URL for the client API (not including _matrix/...)
+              '';
+            };
+
+            tls_certificate_path = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/var/lib/acme/example.com/fullchain.pem";
+              description = ''
+                PEM encoded X509 certificate for TLS.
+                You can replace the self-signed certificate that synapse
+                autogenerates on launch with your own SSL certificate + key pair
+                if you like.  Any required intermediary certificates can be
+                appended after the primary certificate in hierarchical order.
+              '';
+            };
+
+            tls_private_key_path = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/var/lib/acme/example.com/key.pem";
+              description = ''
+                PEM encoded private key for TLS. Specify null if synapse is not
+                speaking TLS directly.
+              '';
+            };
+
+            presence.enabled = mkOption {
+              type = types.bool;
+              default = true;
+              example = false;
+              description = ''
+                Whether to enable presence tracking.
+
+                Presence tracking allows users to see the state (e.g online/offline)
+                of other local and remote users.
+              '';
+            };
+
+            listeners = mkOption {
+              type = types.listOf (types.submodule {
+                options = {
+                  port = mkOption {
+                    type = types.port;
+                    example = 8448;
+                    description = ''
+                      The port to listen for HTTP(S) requests on.
+                    '';
+                  };
+
+                  bind_addresses = mkOption {
+                    type = types.listOf types.str;
+                    default = [
+                      "::1"
+                      "127.0.0.1"
+                    ];
+                    example = literalExpression ''
+                    [
+                      "::"
+                      "0.0.0.0"
+                    ]
+                    '';
+                    description = ''
+                     IP addresses to bind the listener to.
+                    '';
+                  };
+
+                  type = mkOption {
+                    type = types.enum [
+                      "http"
+                      "manhole"
+                      "metrics"
+                      "replication"
+                    ];
+                    default = "http";
+                    example = "metrics";
+                    description = ''
+                      The type of the listener, usually http.
+                    '';
+                  };
+
+                  tls = mkOption {
+                    type = types.bool;
+                    default = true;
+                    example = false;
+                    description = ''
+                      Whether to enable TLS on the listener socket.
+                    '';
+                  };
+
+                  x_forwarded = mkOption {
+                    type = types.bool;
+                    default = false;
+                    example = true;
+                    description = ''
+                      Use the X-Forwarded-For (XFF) header as the client IP and not the
+                      actual client IP.
+                    '';
+                  };
+
+                  resources = mkOption {
+                    type = types.listOf (types.submodule {
+                      options = {
+                        names = mkOption {
+                          type = types.listOf (types.enum [
+                            "client"
+                            "consent"
+                            "federation"
+                            "keys"
+                            "media"
+                            "metrics"
+                            "openid"
+                            "replication"
+                            "static"
+                          ]);
+                          description = ''
+                            List of resources to host on this listener.
+                          '';
+                          example = [
+                            "client"
+                          ];
+                        };
+                        compress = mkOption {
+                          type = types.bool;
+                          description = ''
+                            Should synapse compress HTTP responses to clients that support it?
+                            This should be disabled if running synapse behind a load balancer
+                            that can do automatic compression.
+                          '';
+                        };
+                      };
+                    });
+                    description = ''
+                      List of HTTP resources to serve on this listener.
+                    '';
+                  };
+                };
+              });
+              default = [ {
+                port = 8008;
+                bind_addresses = [ "127.0.0.1" ];
+                type = "http";
+                tls = false;
+                x_forwarded = true;
+                resources = [ {
+                  names = [ "client" ];
+                  compress = true;
+                } {
+                  names = [ "federation" ];
+                  compress = false;
+                } ];
+              } ];
+              description = ''
+                List of ports that Synapse should listen on, their purpose and their configuration.
+              '';
+            };
+
+            database.name = mkOption {
+              type = types.enum [
+                "sqlite3"
+                "psycopg2"
+              ];
+              default = if versionAtLeast config.system.stateVersion "18.03"
+                then "psycopg2"
+                else "sqlite3";
+               defaultText = literalExpression ''
+                if versionAtLeast config.system.stateVersion "18.03"
+                then "psycopg2"
+                else "sqlite3"
+              '';
+              description = ''
+                The database engine name. Can be sqlite3 or psycopg2.
+              '';
+            };
+
+            database.args.database = mkOption {
+              type = types.str;
+              default = {
+                sqlite3 = "${cfg.dataDir}/homeserver.db";
+                psycopg2 = "matrix-synapse";
+              }.${cfg.settings.database.name};
+              defaultText = literalExpression ''
+              {
+                sqlite3 = "''${${options.services.matrix-synapse.dataDir}}/homeserver.db";
+                psycopg2 = "matrix-synapse";
+              }.''${${options.services.matrix-synapse.settings}.database.name};
+              '';
+              description = ''
+                Name of the database when using the psycopg2 backend,
+                path to the database location when using sqlite3.
+              '';
+            };
+
+            database.args.user = mkOption {
+              type = types.nullOr types.str;
+              default = {
+                sqlite3 = null;
+                psycopg2 = "matrix-synapse";
+              }.${cfg.settings.database.name};
+              description = ''
+                Username to connect with psycopg2, set to null
+                when using sqlite3.
+              '';
+            };
+
+            url_preview_enabled = mkOption {
+              type = types.bool;
+              default = true;
+              example = false;
+              description = ''
+                Is the preview URL API enabled?  If enabled, you *must* specify an
+                explicit url_preview_ip_range_blacklist of IPs that the spider is
+                denied from accessing.
+              '';
+            };
+
+            url_preview_ip_range_blacklist = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "10.0.0.0/8"
+                "100.64.0.0/10"
+                "127.0.0.0/8"
+                "169.254.0.0/16"
+                "172.16.0.0/12"
+                "192.0.0.0/24"
+                "192.0.2.0/24"
+                "192.168.0.0/16"
+                "192.88.99.0/24"
+                "198.18.0.0/15"
+                "198.51.100.0/24"
+                "2001:db8::/32"
+                "203.0.113.0/24"
+                "224.0.0.0/4"
+                "::1/128"
+                "fc00::/7"
+                "fe80::/10"
+                "fec0::/10"
+                "ff00::/8"
+              ];
+              description = ''
+                List of IP address CIDR ranges that the URL preview spider is denied
+                from accessing.
+              '';
+            };
+
+            url_preview_ip_range_whitelist = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                List of IP address CIDR ranges that the URL preview spider is allowed
+                to access even if they are specified in url_preview_ip_range_blacklist.
+              '';
+            };
+
+            url_preview_url_blacklist = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                Optional list of URL matches that the URL preview spider is
+                denied from accessing.
+              '';
+            };
+
+            max_upload_size = mkOption {
+              type = types.str;
+              default = "50M";
+              example = "100M";
+              description = ''
+                The largest allowed upload size in bytes
+              '';
+            };
+
+            max_image_pixels = mkOption {
+              type = types.str;
+              default = "32M";
+              example = "64M";
+              description = ''
+                Maximum number of pixels that will be thumbnailed
+              '';
+            };
+
+            dynamic_thumbnails = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = ''
+                Whether to generate new thumbnails on the fly to precisely match
+                the resolution requested by the client. If true then whenever
+                a new resolution is requested by the client the server will
+                generate a new thumbnail. If false the server will pick a thumbnail
+                from a precalculated list.
+              '';
+            };
+
+            turn_uris = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [
+                "turn:turn.example.com:3487?transport=udp"
+                "turn:turn.example.com:3487?transport=tcp"
+                "turns:turn.example.com:5349?transport=udp"
+                "turns:turn.example.com:5349?transport=tcp"
+              ];
+              description = ''
+                The public URIs of the TURN server to give to clients
+              '';
+            };
+            turn_shared_secret = mkOption {
+              type = types.str;
+              default = "";
+              example = literalExpression ''
+                config.services.coturn.static-auth-secret
+              '';
+              description = mdDoc ''
+                The shared secret used to compute passwords for the TURN server.
+
+                Secrets should be passed in via `extraConfigFiles`!
+              '';
+            };
+
+            trusted_key_servers = mkOption {
+              type = types.listOf (types.submodule {
+                options = {
+                  server_name = mkOption {
+                    type = types.str;
+                    example = "matrix.org";
+                    description = ''
+                      Hostname of the trusted server.
+                    '';
+                  };
+
+                  verify_keys = mkOption {
+                    type = types.nullOr (types.attrsOf types.str);
+                    default = null;
+                    example = literalExpression ''
+                      {
+                        "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
+                      }
+                    '';
+                    description = ''
+                      Attribute set from key id to base64 encoded public key.
+
+                      If specified synapse will check that the response is signed
+                      by at least one of the given keys.
+                    '';
+                  };
+                };
+              });
+              default = [ {
+                server_name = "matrix.org";
+                verify_keys = {
+                  "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
+                };
+              } ];
+              description = ''
+                The trusted servers to download signing keys from.
+              '';
+            };
+
+            app_service_config_files = mkOption {
+              type = types.listOf types.path;
+              default = [ ];
+              description = ''
+                A list of application service config file to use
+              '';
+            };
+
+          };
+        };
+      };
+
+      extraConfigFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Extra config files to include.
+
+          The configuration files will be included based on the command line
+          argument --config-path. This allows to configure secrets without
+          having to go through the Nix store, e.g. based on deployment keys if
+          NixOps is in use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
+        message = ''
+          Cannot deploy matrix-synapse with a configuration for a local postgresql database
+            and a missing postgresql service. Since 20.03 it's mandatory to manually configure the
+            database (please read the thread in https://github.com/NixOS/nixpkgs/pull/80447 for
+            further reference).
+
+            If you
+            - try to deploy a fresh synapse, you need to configure the database yourself. An example
+              for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix>
+            - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true`
+              to your configuration.
+
+          For further information about this update, please read the release-notes of 20.03 carefully.
+        '';
+      }
+    ];
+
+    services.matrix-synapse.configFile = configFile;
+
+    users.users.matrix-synapse = {
+      group = "matrix-synapse";
+      home = cfg.dataDir;
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      uid = config.ids.uids.matrix-synapse;
+    };
+
+    users.groups.matrix-synapse = {
+      gid = config.ids.gids.matrix-synapse;
+    };
+
+    systemd.services.matrix-synapse = {
+      description = "Synapse Matrix homeserver";
+      after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        ${cfg.package}/bin/synapse_homeserver \
+          --config-path ${configFile} \
+          --keys-directory ${cfg.dataDir} \
+          --generate-keys
+      '';
+      environment = {
+        PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      } // optionalAttrs (cfg.withJemalloc) {
+        LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+      };
+      serviceConfig = {
+        Type = "notify";
+        User = "matrix-synapse";
+        Group = "matrix-synapse";
+        WorkingDirectory = cfg.dataDir;
+        ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
+          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
+          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
+        '')) ];
+        ExecStart = ''
+          ${cfg.package}/bin/synapse_homeserver \
+            ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
+            --keys-directory ${cfg.dataDir}
+        '';
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
+        Restart = "on-failure";
+        UMask = "0077";
+      };
+    };
+
+    environment.systemPackages = [ registerNewMatrixUser ];
+  };
+
+  meta = {
+    buildDocsInSandbox = false;
+    doc = ./synapse.xml;
+    maintainers = teams.matrix.members;
+  };
+
+}
diff --git a/nixos/modules/services/matrix/synapse.xml b/nixos/modules/services/matrix/synapse.xml
new file mode 100644
index 0000000000000..65bc53d33ac34
--- /dev/null
+++ b/nixos/modules/services/matrix/synapse.xml
@@ -0,0 +1,276 @@
+<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-matrix">
+ <title>Matrix</title>
+ <para>
+  <link xlink:href="https://matrix.org/">Matrix</link> is an open standard for
+  interoperable, decentralised, real-time communication over IP. It can be used
+  to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
+  communication - or anywhere you need a standard HTTP API for publishing and
+  subscribing to data whilst tracking the conversation history.
+ </para>
+ <para>
+  This chapter will show you how to set up your own, self-hosted Matrix
+  homeserver using the Synapse reference homeserver, and how to serve your own
+  copy of the Element web client. See the
+  <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
+  Matrix Now!</link> overview page for links to Element Apps for Android and iOS,
+  desktop clients, as well as bridges to other networks and other projects
+  around Matrix.
+ </para>
+ <section xml:id="module-services-matrix-synapse">
+  <title>Synapse Homeserver</title>
+
+  <para>
+   <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link> is
+   the reference homeserver implementation of Matrix from the core development
+   team at matrix.org. The following configuration example will set up a
+   synapse server for the <literal>example.org</literal> domain, served from
+   the host <literal>myhostname.example.org</literal>. For more information,
+   please refer to the
+   <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
+   installation instructions of Synapse </link>.
+<programlisting>
+{ pkgs, lib, config, ... }:
+let
+  fqdn = "${config.networking.hostName}.${config.networking.domain}";
+  clientConfig = {
+    "m.homeserver".base_url = "https://${fqdn}";
+    "m.identity_server" = {};
+  };
+  serverConfig."m.server" = "${config.services.matrix-synapse.settings.server_name}:443";
+  mkWellKnown = data: ''
+    add_header Content-Type application/json;
+    add_header Access-Control-Allow-Origin *;
+    return 200 '${builtins.toJSON data}';
+  '';
+in {
+  <xref linkend="opt-networking.hostName" /> = "myhostname";
+  <xref linkend="opt-networking.domain" /> = "example.org";
+  <xref linkend="opt-networking.firewall.allowedTCPPorts" /> = [ 80 443 ];
+
+  <xref linkend="opt-services.postgresql.enable" /> = true;
+  <xref linkend="opt-services.postgresql.initialScript" /> = pkgs.writeText "synapse-init.sql" ''
+    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+      TEMPLATE template0
+      LC_COLLATE = "C"
+      LC_CTYPE = "C";
+  '';
+
+  services.nginx = {
+    <link linkend="opt-services.nginx.enable">enable</link> = true;
+    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
+    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+      "${config.networking.domain}" = { <co xml:id='ex-matrix-synapse-dns' />
+        <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> = mkWellKnown serverConfig; <co xml:id='ex-matrix-synapse-well-known-server' />
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> = mkWellKnown clientConfig; <co xml:id='ex-matrix-synapse-well-known-client' />
+      };
+      "${fqdn}" = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = '' <co xml:id='ex-matrix-synapse-rev-default' />
+          return 404;
+        '';
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/_matrix".proxyPass</link> = "http://[::1]:8008"; <co xml:id='ex-matrix-synapse-rev-proxy-pass' />
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/_synapse/client".proxyPass</link> = "http://[::1]:8008"; <co xml:id='ex-matrix-synapse-rev-client' />
+      };
+    };
+  };
+
+  services.matrix-synapse = {
+    <link linkend="opt-services.matrix-synapse.enable">enable</link> = true;
+    <link linkend="opt-services.matrix-synapse.settings.server_name">settings.server_name</link> = config.networking.domain;
+    <link linkend="opt-services.matrix-synapse.settings.listeners">settings.listeners</link> = [
+      { <link linkend="opt-services.matrix-synapse.settings.listeners._.port">port</link> = 8008;
+        <link linkend="opt-services.matrix-synapse.settings.listeners._.bind_addresses">bind_addresses</link> = [ "::1" ];
+        <link linkend="opt-services.matrix-synapse.settings.listeners._.type">type</link> = "http";
+        <link linkend="opt-services.matrix-synapse.settings.listeners._.tls">tls</link> = false;
+        <link linkend="opt-services.matrix-synapse.settings.listeners._.x_forwarded">x_forwarded</link> = true;
+        <link linkend="opt-services.matrix-synapse.settings.listeners._.resources">resources</link> = [ {
+          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.names">names</link> = [ "client" "federation" ];
+          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = true;
+        } ];
+      }
+    ];
+  };
+}
+</programlisting>
+  </para>
+  <calloutlist>
+   <callout arearefs='ex-matrix-synapse-dns'>
+    <para>
+     If the <code>A</code> and <code>AAAA</code> DNS records on
+     <literal>example.org</literal> do not point on the same host as the records
+     for <code>myhostname.example.org</code>, you can easily move the
+     <code>/.well-known</code> virtualHost section of the code to the host that
+     is serving <literal>example.org</literal>, while the rest stays on
+     <literal>myhostname.example.org</literal> with no other changes required.
+     This pattern also allows to seamlessly move the homeserver from
+     <literal>myhostname.example.org</literal> to
+     <literal>myotherhost.example.org</literal> by only changing the
+     <code>/.well-known</code> redirection target.
+    </para>
+   </callout>
+   <callout arearefs='ex-matrix-synapse-well-known-server'>
+    <para>
+     This section is not needed if the <link linkend="opt-services.matrix-synapse.settings.server_name">server_name</link>
+     of <package>matrix-synapse</package> is equal to the domain (i.e.
+     <literal>example.org</literal> from <literal>@foo:example.org</literal>)
+     and the federation port is 8448.
+     Further reference can be found in the <link xlink:href="https://matrix-org.github.io/synapse/latest/delegate.html">docs
+     about delegation</link>.
+    </para>
+   </callout>
+   <callout arearefs='ex-matrix-synapse-well-known-client'>
+    <para>
+     This is usually needed for homeserver discovery (from e.g. other Matrix clients).
+     Further reference can be found in the <link xlink:href="https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient">upstream docs</link>
+    </para>
+   </callout>
+   <callout arearefs='ex-matrix-synapse-rev-default'>
+    <para>
+     It's also possible to do a redirect here or something else, this vhost is not
+     needed for Matrix. It's recommended though to <emphasis>not put</emphasis> element
+     here, see also the <link linkend='ex-matrix-synapse-rev-default'>section about Element</link>.
+    </para>
+   </callout>
+   <callout arearefs='ex-matrix-synapse-rev-proxy-pass'>
+    <para>
+     Forward all Matrix API calls to the synapse Matrix homeserver. A trailing slash
+     <emphasis>must not</emphasis> be used here.
+    </para>
+   </callout>
+   <callout arearefs='ex-matrix-synapse-rev-client'>
+    <para>
+     Forward requests for e.g. SSO and password-resets.
+    </para>
+   </callout>
+  </calloutlist>
+ </section>
+ <section xml:id="module-services-matrix-register-users">
+  <title>Registering Matrix users</title>
+  <para>
+   If you want to run a server with public registration by anybody, you can
+   then enable <literal><link linkend="opt-services.matrix-synapse.settings.enable_registration">services.matrix-synapse.settings.enable_registration</link> =
+   true;</literal>. Otherwise, or you can generate a registration secret with
+   <command>pwgen -s 64 1</command> and set it with
+   <option><link linkend="opt-services.matrix-synapse.settings.registration_shared_secret">services.matrix-synapse.settings.registration_shared_secret</link></option>.
+   To create a new user or admin, run the following after you have set the secret
+   and have rebuilt NixOS:
+<screen>
+<prompt>$ </prompt>nix-shell -p matrix-synapse
+<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008
+<prompt>New user localpart: </prompt><replaceable>your-username</replaceable>
+<prompt>Password:</prompt>
+<prompt>Confirm password:</prompt>
+<prompt>Make admin [no]:</prompt>
+Success!
+</screen>
+   In the example, this would create a user with the Matrix Identifier
+   <literal>@your-username:example.org</literal>.
+   <warning>
+    <para>
+     When using <xref linkend="opt-services.matrix-synapse.settings.registration_shared_secret" />, the secret
+     will end up in the world-readable store. Instead it's recommended to deploy the secret
+     in an additional file like this:
+     <itemizedlist>
+      <listitem>
+       <para>
+        Create a file with the following contents:
+<programlisting>registration_shared_secret: your-very-secret-secret</programlisting>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+        Deploy the file with a secret-manager such as <link xlink:href="https://nixops.readthedocs.io/en/latest/overview.html#managing-keys"><option>deployment.keys</option></link>
+        from <citerefentry><refentrytitle>nixops</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+        or <link xlink:href="https://github.com/Mic92/sops-nix/">sops-nix</link> to
+        e.g. <filename>/run/secrets/matrix-shared-secret</filename> and ensure that it's readable
+        by <package>matrix-synapse</package>.
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+        Include the file like this in your configuration:
+<programlisting>
+{
+  <xref linkend="opt-services.matrix-synapse.extraConfigFiles" /> = [
+    "/run/secrets/matrix-shared-secret"
+  ];
+}
+</programlisting>
+       </para>
+      </listitem>
+     </itemizedlist>
+    </para>
+   </warning>
+  </para>
+  <note>
+   <para>
+    It's also possible to user alternative authentication mechanism such as
+    <link xlink:href="https://github.com/matrix-org/matrix-synapse-ldap3">LDAP (via <literal>matrix-synapse-ldap3</literal>)</link>
+    or <link xlink:href="https://matrix-org.github.io/synapse/latest/openid.html">OpenID</link>.
+   </para>
+  </note>
+ </section>
+ <section xml:id="module-services-matrix-element-web">
+  <title>Element (formerly known as Riot) Web Client</title>
+
+  <para>
+   <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is
+   the reference web client for Matrix and developed by the core team at
+   matrix.org. Element was formerly known as Riot.im, see the
+   <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link>
+   for more information. The following snippet can be optionally added to the code before
+   to complete the synapse installation with a web client served at
+   <code>https://element.myhostname.example.org</code> and
+   <code>https://element.example.org</code>. Alternatively, you can use the hosted
+   copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>,
+   or use other web clients or native client applications. Due to the
+   <literal>/.well-known</literal> urls set up done above, many clients should
+   fill in the required connection details automatically when you enter your
+   Matrix Identifier. See
+   <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
+   Matrix Now!</link> for a list of existing clients and their supported
+   featureset.
+<programlisting>
+{
+  services.nginx.virtualHosts."element.${fqdn}" = {
+    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
+      "element.${config.networking.domain}"
+    ];
+
+    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
+      conf = {
+        default_server_config = clientConfig; # see `clientConfig` from the snippet above.
+      };
+    };
+  };
+}
+</programlisting>
+  </para>
+
+  <note>
+   <para>
+    The Element developers do not recommend running Element and your Matrix
+    homeserver on the same fully-qualified domain name for security reasons. In
+    the example, this means that you should not reuse the
+    <literal>myhostname.example.org</literal> virtualHost to also serve Element,
+    but instead serve it on a different subdomain, like
+    <literal>element.example.org</literal> in the example. See the
+    <link xlink:href="https://github.com/vector-im/element-web/tree/v1.10.0#important-security-notes">Element
+    Important Security Notes</link> for more information on this subject.
+   </para>
+  </note>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 5a5c30a412330..2b9c6d80abbd7 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -39,9 +39,11 @@ in {
         default = "127.0.0.1";
         description = ''
           The host name or IP address on which to bind Airsonic.
-          Only relevant if you have multiple network interfaces and want
-          to make Airsonic available on only one of them. The default value
-          will bind Airsonic to all available network interfaces.
+          The default value is appropriate for first launch, when the
+          default credentials are easy to guess. It is also appropriate
+          if you intend to use the virtualhost option in the service
+          module. In other cases, you may want to change this to a
+          specific IP or 0.0.0.0 to listen on all interfaces.
         '';
       };
 
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index a65c5c9d11cf7..ef799e9ce3b63 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -5,6 +5,243 @@ with lib;
 let
 
   cfg = config.services.autorandr;
+  hookType = types.lines;
+
+  matrixOf = n: m: elemType:
+  mkOptionType rec {
+    name = "matrixOf";
+    description =
+      "${toString n}×${toString m} matrix of ${elemType.description}s";
+    check = xss:
+      let listOfSize = l: xs: isList xs && length xs == l;
+      in listOfSize n xss
+      && all (xs: listOfSize m xs && all elemType.check xs) xss;
+    merge = mergeOneOption;
+    getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
+    getSubModules = elemType.getSubModules;
+    substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
+    functor = (defaultFunctor name) // { wrapped = elemType; };
+  };
+
+  profileModule = types.submodule {
+    options = {
+      fingerprint = mkOption {
+        type = types.attrsOf types.str;
+        description = ''
+          Output name to EDID mapping.
+          Use <code>autorandr --fingerprint</code> to get current setup values.
+        '';
+        default = { };
+      };
+
+      config = mkOption {
+        type = types.attrsOf configModule;
+        description = "Per output profile configuration.";
+        default = { };
+      };
+
+      hooks = mkOption {
+        type = hooksModule;
+        description = "Profile hook scripts.";
+        default = { };
+      };
+    };
+  };
+
+  configModule = types.submodule {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        description = "Whether to enable the output.";
+        default = true;
+      };
+
+      crtc = mkOption {
+        type = types.nullOr types.ints.unsigned;
+        description = "Output video display controller.";
+        default = null;
+        example = 0;
+      };
+
+      primary = mkOption {
+        type = types.bool;
+        description = "Whether output should be marked as primary";
+        default = false;
+      };
+
+      position = mkOption {
+        type = types.str;
+        description = "Output position";
+        default = "";
+        example = "5760x0";
+      };
+
+      mode = mkOption {
+        type = types.str;
+        description = "Output resolution.";
+        default = "";
+        example = "3840x2160";
+      };
+
+      rate = mkOption {
+        type = types.str;
+        description = "Output framerate.";
+        default = "";
+        example = "60.00";
+      };
+
+      gamma = mkOption {
+        type = types.str;
+        description = "Output gamma configuration.";
+        default = "";
+        example = "1.0:0.909:0.833";
+      };
+
+      rotate = mkOption {
+        type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
+        description = "Output rotate configuration.";
+        default = null;
+        example = "left";
+      };
+
+      transform = mkOption {
+        type = types.nullOr (matrixOf 3 3 types.float);
+        default = null;
+        example = literalExpression ''
+          [
+            [ 0.6 0.0 0.0 ]
+            [ 0.0 0.6 0.0 ]
+            [ 0.0 0.0 1.0 ]
+          ]
+        '';
+        description = ''
+          Refer to
+          <citerefentry>
+            <refentrytitle>xrandr</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          for the documentation of the transform matrix.
+        '';
+      };
+
+      dpi = mkOption {
+        type = types.nullOr types.ints.positive;
+        description = "Output DPI configuration.";
+        default = null;
+        example = 96;
+      };
+
+      scale = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            method = mkOption {
+              type = types.enum [ "factor" "pixel" ];
+              description = "Output scaling method.";
+              default = "factor";
+              example = "pixel";
+            };
+
+            x = mkOption {
+              type = types.either types.float types.ints.positive;
+              description = "Horizontal scaling factor/pixels.";
+            };
+
+            y = mkOption {
+              type = types.either types.float types.ints.positive;
+              description = "Vertical scaling factor/pixels.";
+            };
+          };
+        });
+        description = ''
+          Output scale configuration.
+          </para><para>
+          Either configure by pixels or a scaling factor. When using pixel method the
+          <citerefentry>
+            <refentrytitle>xrandr</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          option
+          <parameter class="command">--scale-from</parameter>
+          will be used; when using factor method the option
+          <parameter class="command">--scale</parameter>
+          will be used.
+          </para><para>
+          This option is a shortcut version of the transform option and they are mutually
+          exclusive.
+        '';
+        default = null;
+        example = literalExpression ''
+          {
+            x = 1.25;
+            y = 1.25;
+          }
+        '';
+      };
+    };
+  };
+
+  hooksModule = types.submodule {
+    options = {
+      postswitch = mkOption {
+        type = types.attrsOf hookType;
+        description = "Postswitch hook executed after mode switch.";
+        default = { };
+      };
+
+      preswitch = mkOption {
+        type = types.attrsOf hookType;
+        description = "Preswitch hook executed before mode switch.";
+        default = { };
+      };
+
+      predetect = mkOption {
+        type = types.attrsOf hookType;
+        description = ''
+          Predetect hook executed before autorandr attempts to run xrandr.
+        '';
+        default = { };
+      };
+    };
+  };
+
+  hookToFile = folder: name: hook:
+    nameValuePair "xdg/autorandr/${folder}/${name}" {
+      source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
+    };
+  profileToFiles = name: profile:
+    with profile;
+    mkMerge ([
+      {
+        "xdg/autorandr/${name}/setup".text = concatStringsSep "\n"
+          (mapAttrsToList fingerprintToString fingerprint);
+        "xdg/autorandr/${name}/config".text =
+          concatStringsSep "\n" (mapAttrsToList configToString profile.config);
+      }
+      (mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
+      (mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
+      (mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
+    ]);
+  fingerprintToString = name: edid: "${name} ${edid}";
+  configToString = name: config:
+    if config.enable then
+      concatStringsSep "\n" ([ "output ${name}" ]
+        ++ optional (config.position != "") "pos ${config.position}"
+        ++ optional (config.crtc != null) "crtc ${toString config.crtc}"
+        ++ optional config.primary "primary"
+        ++ optional (config.dpi != null) "dpi ${toString config.dpi}"
+        ++ optional (config.gamma != "") "gamma ${config.gamma}"
+        ++ optional (config.mode != "") "mode ${config.mode}"
+        ++ optional (config.rate != "") "rate ${config.rate}"
+        ++ optional (config.rotate != null) "rotate ${config.rotate}"
+        ++ optional (config.transform != null) ("transform "
+          + concatMapStringsSep "," toString (flatten config.transform))
+        ++ optional (config.scale != null)
+        ((if config.scale.method == "factor" then "scale" else "scale-from")
+          + " ${toString config.scale.x}x${toString config.scale.y}"))
+    else ''
+      output ${name}
+      off
+    '';
 
 in {
 
@@ -22,6 +259,67 @@ in {
           for further reference.
         '';
       };
+
+      hooks = mkOption {
+        type = hooksModule;
+        description = "Global hook scripts";
+        default = { };
+        example = ''
+          {
+            postswitch = {
+              "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
+              "change-background" = readFile ./change-background.sh;
+              "change-dpi" = '''
+                case "$AUTORANDR_CURRENT_PROFILE" in
+                  default)
+                    DPI=120
+                    ;;
+                  home)
+                    DPI=192
+                    ;;
+                  work)
+                    DPI=144
+                    ;;
+                  *)
+                    echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
+                    exit 1
+                esac
+                echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
+              '''
+            };
+          }
+        '';
+      };
+      profiles = mkOption {
+        type = types.attrsOf profileModule;
+        description = "Autorandr profiles specification.";
+        default = { };
+        example = literalExpression ''
+          {
+            "work" = {
+              fingerprint = {
+                eDP1 = "<EDID>";
+                DP1 = "<EDID>";
+              };
+              config = {
+                eDP1.enable = false;
+                DP1 = {
+                  enable = true;
+                  crtc = 0;
+                  primary = true;
+                  position = "0x0";
+                  mode = "3840x2160";
+                  gamma = "1.0:0.909:0.833";
+                  rate = "60.00";
+                  rotate = "left";
+                };
+              };
+              hooks.postswitch = readFile ./work-postswitch.sh;
+            };
+          }
+        '';
+      };
+
     };
 
   };
@@ -30,7 +328,15 @@ in {
 
     services.udev.packages = [ pkgs.autorandr ];
 
-    environment.systemPackages = [ pkgs.autorandr ];
+    environment = {
+      systemPackages = [ pkgs.autorandr ];
+      etc = mkMerge ([
+        (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
+        (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
+        (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
+        (mkMerge (mapAttrsToList profileToFiles cfg.profiles))
+      ]);
+    };
 
     systemd.services.autorandr = {
       wantedBy = [ "sleep.target" ];
@@ -49,5 +355,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ ];
+  meta.maintainers = with maintainers; [ alexnortung ];
 }
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
index 96e2a4e7c2602..8cb51bb0b7a7f 100644
--- a/nixos/modules/services/misc/dictd.nix
+++ b/nixos/modules/services/misc/dictd.nix
@@ -45,6 +45,10 @@ in
     # get the command line client on system path to make some use of the service
     environment.systemPackages = [ pkgs.dict ];
 
+    environment.etc."dict.conf".text = ''
+      server localhost
+    '';
+
     users.users.dictd =
       { group = "dictd";
         description = "DICT.org dictd server";
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
index dd84ac37b0d5e..cb99364aa1a60 100644
--- a/nixos/modules/services/misc/etebase-server.nix
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -166,7 +166,7 @@ in
       } ''
         makeWrapper ${pythonEnv}/bin/etebase-server \
           $out/bin/etebase-server \
-          --run "cd ${cfg.dataDir}" \
+          --chdir ${escapeShellArg cfg.dataDir} \
           --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
       '')
     ];
diff --git a/nixos/modules/services/misc/ethminer.nix b/nixos/modules/services/misc/ethminer.nix
index 95afb0460fb8e..2236346698288 100644
--- a/nixos/modules/services/misc/ethminer.nix
+++ b/nixos/modules/services/misc/ethminer.nix
@@ -22,7 +22,7 @@ in
       };
 
       recheckInterval = mkOption {
-        type = types.int;
+        type = types.ints.unsigned;
         default = 2000;
         description = "Interval in milliseconds between farm rechecks.";
       };
@@ -70,7 +70,7 @@ in
       };
 
       maxPower = mkOption {
-        type = types.int;
+        type = types.ints.unsigned;
         default = 113;
         description = "Miner max watt usage.";
       };
@@ -85,7 +85,7 @@ in
   config = mkIf cfg.enable {
 
     systemd.services.ethminer = {
-      path = [ pkgs.cudatoolkit ];
+      path = optional (cfg.toolkit == "cuda") [ pkgs.cudaPackages.cudatoolkit ];
       description = "ethminer ethereum mining service";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
@@ -97,7 +97,7 @@ in
         Restart = "always";
       };
 
-      environment = {
+      environment = mkIf (cfg.toolkit == "cuda") {
         LD_LIBRARY_PATH = "${config.boot.kernelPackages.nvidia_x11}/lib";
       };
 
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
index 3211d4d88e4d9..db643c3d84795 100644
--- a/nixos/modules/services/misc/geoipupdate.nix
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -2,6 +2,7 @@
 
 let
   cfg = config.services.geoipupdate;
+  inherit (builtins) isAttrs isString isInt isList typeOf hashString;
 in
 {
   imports = [
@@ -27,11 +28,30 @@ in
       };
 
       settings = lib.mkOption {
+        example = lib.literalExpression ''
+          {
+            AccountID = 200001;
+            DatabaseDirectory = "/var/lib/GeoIP";
+            LicenseKey = { _secret = "/run/keys/maxmind_license_key"; };
+            Proxy = "10.0.0.10:8888";
+            ProxyUserPassword = { _secret = "/run/keys/proxy_pass"; };
+          }
+        '';
         description = ''
           <productname>geoipupdate</productname> configuration
           options. See
           <link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
           for a full list of available options.
+
+          Settings containing secret data should be set to an
+          attribute set containing the attribute
+          <literal>_secret</literal> - a string pointing to a file
+          containing the value the option should be set to. See the
+          example to get a better picture of this: in the resulting
+          <filename>GeoIP.conf</filename> file, the
+          <literal>ProxyUserPassword</literal> key will be set to the
+          contents of the
+          <filename>/run/keys/proxy_pass</filename> file.
         '';
         type = lib.types.submodule {
           freeformType =
@@ -65,11 +85,18 @@ in
             };
 
             LicenseKey = lib.mkOption {
-              type = lib.types.path;
+              type = with lib.types; either path (attrsOf path);
               description = ''
-                A file containing the <productname>MaxMind</productname>
-                license key.
+                A file containing the
+                <productname>MaxMind</productname> license key.
+
+                Always handled as a secret whether the value is
+                wrapped in a <literal>{ _secret = ...; }</literal>
+                attrset or not (refer to <xref
+                linkend="opt-services.geoipupdate.settings" /> for
+                details).
               '';
+              apply = x: if isAttrs x then x else { _secret = x; };
             };
 
             DatabaseDirectory = lib.mkOption {
@@ -102,6 +129,9 @@ in
     systemd.services.geoipupdate-create-db-dir = {
       serviceConfig.Type = "oneshot";
       script = ''
+        set -o errexit -o pipefail -o nounset -o errtrace
+        shopt -s inherit_errexit
+
         mkdir -p ${cfg.settings.DatabaseDirectory}
         chmod 0755 ${cfg.settings.DatabaseDirectory}
       '';
@@ -115,32 +145,41 @@ in
         "network-online.target"
         "nss-lookup.target"
       ];
+      path = [ pkgs.replace-secret ];
       wants = [ "network-online.target" ];
       startAt = cfg.interval;
       serviceConfig = {
         ExecStartPre =
           let
+            isSecret = v: isAttrs v && v ? _secret && isString v._secret;
             geoipupdateKeyValue = lib.generators.toKeyValue {
               mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
-                mkValueString = v: with builtins;
+                mkValueString = v:
                   if isInt           v then toString v
                   else if isString   v then v
                   else if true  ==   v then "1"
                   else if false ==   v then "0"
                   else if isList     v then lib.concatMapStringsSep " " mkValueString v
+                  else if isSecret   v then hashString "sha256" v._secret
                   else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
               };
             };
+            secretPaths = lib.catAttrs "_secret" (lib.collect isSecret cfg.settings);
+            mkSecretReplacement = file: ''
+              replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/geoipupdate/GeoIP.conf" ]}
+            '';
+            secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
 
             geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
 
             script = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
               chown geoip "${cfg.settings.DatabaseDirectory}"
 
               cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
-              ${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
-                                                        '${cfg.settings.LicenseKey}' \
-                                                        /run/geoipupdate/GeoIP.conf
+              ${secretReplacements}
             '';
           in
             "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index bc7bb663ee007..effa0c06ad6c5 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -499,6 +499,7 @@ in
         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";
+        replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
       in ''
         # copy custom configuration and generate a random secret key if needed
         ${optionalString (cfg.useWizard == false) ''
@@ -526,23 +527,17 @@ in
                 ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
             fi
 
-            SECRETKEY="$(head -n1 ${secretKey})"
-            DBPASS="$(head -n1 ${cfg.database.passwordFile})"
-            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} || :)"
+            chmod u+w '${runConfig}'
+            ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}'
+            ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}'
+            ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}'
+            ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
+            ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}'
+
+            ${lib.optionalString (cfg.mailerPasswordFile != null) ''
+              ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
             ''}
-            sed -e "s,#secretkey#,$SECRETKEY,g" \
-                -e "s,#dbpass#,$DBPASS,g" \
-                -e "s,#oauth2jwtsecret#,$OAUTH2JWTSECRET,g" \
-                -e "s,#lfsjwtsecret#,$LFSJWTSECRET,g" \
-                -e "s,#internaltoken#,$INTERNALTOKEN,g" \
-                -e "s,#mailerpass#,$MAILERPASSWORD,g" \
-                -i ${runConfig}
+            chmod u-w '${runConfig}'
           }
           (umask 027; gitea_setup)
         ''}
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
index ceb186c0f0492..87dd97166b8eb 100644
--- a/nixos/modules/services/misc/gitit.nix
+++ b/nixos/modules/services/misc/gitit.nix
@@ -10,7 +10,7 @@ let
 
   toYesNo = b: if b then "yes" else "no";
 
-  gititShared = with cfg.haskellPackages; gitit + "/share/" + pkgs.stdenv.hostPlatform.system + "-" + ghc.name + "/" + gitit.pname + "-" + gitit.version;
+  gititShared = with cfg.haskellPackages; gitit + "/share/" + ghc.targetPrefix + ghc.haskellCompilerName + "/" + gitit.pname + "-" + gitit.version;
 
   gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
 
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 19f270d373f51..77556bfe00707 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -13,12 +13,28 @@ let
                       else
                         pkgs.postgresql_12;
 
+  # Git 2.36.1 seemingly contains a commit-graph related bug which is
+  # easily triggered through GitLab, so we downgrade it to 2.35.x
+  # until this issue is solved. See
+  # https://gitlab.com/gitlab-org/gitlab/-/issues/360783#note_992870101.
+  gitPackage =
+    let
+      version = "2.35.4";
+    in
+      pkgs.git.overrideAttrs (oldAttrs: rec {
+        inherit version;
+        src = pkgs.fetchurl {
+          url = "https://www.kernel.org/pub/software/scm/git/git-${version}.tar.xz";
+          sha256 = "sha256-mv13OdNkXggeKQkJ+47QcJ6lYmcw6Qjri1ZJ2ETCTOk=";
+        };
+      });
+
   gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
   gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
   pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
 
-  databaseConfig = {
-    production = {
+  databaseConfig = let
+    val = {
       adapter = "postgresql";
       database = cfg.databaseName;
       host = cfg.databaseHost;
@@ -26,6 +42,10 @@ let
       encoding = "utf8";
       pool = cfg.databasePool;
     } // cfg.extraDatabaseConfig;
+  in if lib.versionAtLeast (lib.getVersion cfg.packages.gitlab) "15.0" then {
+    production.main = val;
+  } else {
+    production = val;
   };
 
   # We only want to create a database if we're actually going to connect to it.
@@ -37,7 +57,7 @@ let
     prometheus_listen_addr = "localhost:9236"
 
     [git]
-    bin_path = "${pkgs.git}/bin/git"
+    bin_path = "${gitPackage}/bin/git"
 
     [gitaly-ruby]
     dir = "${cfg.packages.gitaly.ruby}"
@@ -72,7 +92,7 @@ let
     redis = {
       bin = "${pkgs.redis}/bin/redis-cli";
       host = "127.0.0.1";
-      port = 6379;
+      port = config.services.redis.servers.gitlab.port;
       database = 0;
       namespace = "resque:gitlab";
     };
@@ -133,7 +153,7 @@ let
       };
       workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
       gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
-      git.bin_path = "git";
+      git.bin_path = "${gitPackage}/bin/git";
       monitoring = {
         ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
         sidekiq_exporter = {
@@ -179,7 +199,7 @@ let
           ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
           --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
           --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \
-          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+          --chdir '${cfg.packages.gitlab}/share/gitlab'
      '';
   };
 
@@ -193,7 +213,7 @@ let
       makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \
           ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
           --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
-          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+          --chdir '${cfg.packages.gitlab}/share/gitlab'
      '';
   };
 
@@ -450,7 +470,8 @@ in {
 
       redisUrl = mkOption {
         type = types.str;
-        default = "redis://localhost:6379/";
+        default = "redis://localhost:${toString config.services.redis.servers.gitlab.port}/";
+        defaultText = literalExpression ''redis://localhost:''${toString config.services.redis.servers.gitlab.port}/'';
         description = "Redis URL for all GitLab services except gitlab-shell";
       };
 
@@ -847,10 +868,7 @@ in {
 
         extraConfig = mkOption {
           type = types.lines;
-          default = ''
-            copytruncate
-            compress
-          '';
+          default = "";
           description = ''
             Extra logrotate config options for this path. Refer to
             <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
@@ -961,7 +979,11 @@ in {
     };
 
     # Redis is required for the sidekiq queue runner.
-    services.redis.enable = mkDefault true;
+    services.redis.servers.gitlab = {
+      enable = mkDefault true;
+      port = mkDefault 31636;
+      bind = mkDefault "127.0.0.1";
+    };
 
     # We use postgres as the main data store.
     services.postgresql = optionalAttrs databaseActuallyCreateLocally {
@@ -972,13 +994,14 @@ in {
     # Enable rotation of log files
     services.logrotate = {
       enable = cfg.logrotate.enable;
-      paths = {
+      settings = {
         gitlab = {
-          path = "${cfg.statePath}/log/*.log";
-          user = cfg.user;
-          group = cfg.group;
+          files = "${cfg.statePath}/log/*.log";
+          su = "${cfg.user} ${cfg.group}";
           frequency = cfg.logrotate.frequency;
-          keep = cfg.logrotate.keep;
+          rotate = cfg.logrotate.keep;
+          copytruncate = true;
+          compress = true;
           extraConfig = cfg.logrotate.extraConfig;
         };
       };
@@ -1040,7 +1063,7 @@ in {
         chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
       '';
 
-      serviceConfig = {
+      unitConfig = {
         ConditionPathExists = "!${cfg.registry.certFile}";
       };
     };
@@ -1181,7 +1204,7 @@ in {
                 fi
 
                 jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                   '.production.password = $ENV.db_password' \
+                   '.${if lib.versionAtLeast (lib.getVersion cfg.packages.gitlab) "15.0" then "production.main" else "production"}.password = $ENV.db_password' \
                    >'${cfg.statePath}/config/database.yml'
               ''
               else ''
@@ -1249,13 +1272,13 @@ in {
     systemd.services.gitlab-sidekiq = {
       after = [
         "network.target"
-        "redis.service"
+        "redis-gitlab.service"
         "postgresql.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ] ++ optional (cfg.databaseHost == "") "postgresql.service";
@@ -1268,7 +1291,7 @@ in {
       });
       path = with pkgs; [
         postgresqlPackage
-        git
+        gitPackage
         ruby
         openssh
         nodejs
@@ -1299,7 +1322,7 @@ in {
       path = with pkgs; [
         openssh
         procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
-        git
+        gitPackage
         cfg.packages.gitaly.rubyEnv
         cfg.packages.gitaly.rubyEnv.wrappedRuby
         gzip
@@ -1344,7 +1367,7 @@ in {
       partOf = [ "gitlab.target" ];
       path = with pkgs; [
         exiftool
-        git
+        gitPackage
         gnutar
         gzip
         openssh
@@ -1370,7 +1393,7 @@ in {
 
     systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
       description = "GitLab incoming mail daemon";
-      after = [ "network.target" "redis.service" "gitlab-config.service" ];
+      after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ];
       bindsTo = [ "gitlab-config.service" ];
       wantedBy = [ "gitlab.target" ];
       partOf = [ "gitlab.target" ];
@@ -1391,12 +1414,12 @@ in {
       after = [
         "gitlab-workhorse.service"
         "network.target"
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-gitlab.service"
         "gitlab-config.service"
         "gitlab-db-config.service"
       ] ++ optional (cfg.databaseHost == "") "postgresql.service";
@@ -1405,7 +1428,7 @@ in {
       environment = gitlabEnv;
       path = with pkgs; [
         postgresqlPackage
-        git
+        gitPackage
         openssh
         nodejs
         procps
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index cad73a871ba6b..5a5f488dc565b 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -44,6 +44,12 @@ in
       description = "Enable uploads of external files";
     };
 
+    user-icons = mkOption {
+      type = types.nullOr (types.enum [ "gravatar" "identicon" ]);
+      default = null;
+      description = "Enable specific user icons for history view";
+    };
+
     emoji = mkOption {
       type = types.bool;
       default = false;
@@ -56,6 +62,18 @@ in
       description = "Use the first h1 as page title";
     };
 
+    no-edit = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Disable editing pages";
+    };
+
+    local-time = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use the browser's local timezone instead of the server's for displaying dates.";
+    };
+
     branch = mkOption {
       type = types.str;
       default = "master";
@@ -110,12 +128,15 @@ in
             ${optionalString cfg.mathjax "--mathjax"} \
             ${optionalString cfg.emoji "--emoji"} \
             ${optionalString cfg.h1-title "--h1-title"} \
+            ${optionalString cfg.no-edit "--no-edit"} \
+            ${optionalString cfg.local-time "--local-time"} \
             ${optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
+            ${optionalString (cfg.user-icons != null) "--user-icons ${cfg.user-icons}"} \
             ${cfg.stateDir}
         '';
       };
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ erictapen ];
+  meta.maintainers = with lib.maintainers; [ erictapen bbenno ];
 }
diff --git a/nixos/modules/services/misc/heisenbridge.nix b/nixos/modules/services/misc/heisenbridge.nix
index 7ce8a23d9af12..deefb061d8b31 100644
--- a/nixos/modules/services/misc/heisenbridge.nix
+++ b/nixos/modules/services/misc/heisenbridge.nix
@@ -204,7 +204,7 @@ in
         NoNewPrivileges = true;
         LockPersonality = true;
         RestrictRealtime = true;
-        SystemCallFilter = ["@system-service" "~@priviledged" "@chown"];
+        SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
         SystemCallArchitectures = "native";
         RestrictAddressFamilies = "AF_INET AF_INET6";
       };
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
deleted file mode 100644
index fc8ce08b2e136..0000000000000
--- a/nixos/modules/services/misc/home-assistant.nix
+++ /dev/null
@@ -1,416 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.home-assistant;
-
-  # cfg.config != null can be assumed here
-  configJSON = pkgs.writeText "configuration.json"
-    (builtins.toJSON (if cfg.applyDefaultConfig then
-    (recursiveUpdate defaultConfig cfg.config) else cfg.config));
-  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-    # Hack to support custom yaml objects,
-    # i.e. secrets: https://www.home-assistant.io/docs/configuration/secrets/
-    sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
-  '';
-
-  lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
-    (builtins.toJSON cfg.lovelaceConfig);
-  lovelaceConfigFile = pkgs.runCommand "ui-lovelace.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${lovelaceConfigJSON} -o $out
-  '';
-
-  availableComponents = cfg.package.availableComponents;
-
-  explicitComponents = cfg.package.extraComponents;
-
-  usedPlatforms = config:
-    if isAttrs config then
-      optional (config ? platform) config.platform
-      ++ concatMap usedPlatforms (attrValues config)
-    else if isList config then
-      concatMap usedPlatforms config
-    else [ ];
-
-  # Given a component "platform", looks up whether it is used in the config
-  # as `platform = "platform";`.
-  #
-  # For example, the component mqtt.sensor is used as follows:
-  # config.sensor = [ {
-  #   platform = "mqtt";
-  #   ...
-  # } ];
-  useComponentPlatform = component: elem component (usedPlatforms cfg.config);
-
-  useExplicitComponent = component: elem component explicitComponents;
-
-  # Returns whether component is used in config or explicitly passed into package
-  useComponent = component:
-    hasAttrByPath (splitString "." component) cfg.config
-    || useComponentPlatform component
-    || useExplicitComponent component;
-
-  # List of components used in config
-  extraComponents = filter useComponent availableComponents;
-
-  package = if (cfg.autoExtraComponents && cfg.config != null)
-    then (cfg.package.override { inherit extraComponents; })
-    else cfg.package;
-
-  # If you are changing this, please update the description in applyDefaultConfig
-  defaultConfig = {
-    homeassistant.time_zone = config.time.timeZone;
-    http.server_port = cfg.port;
-  } // optionalAttrs (cfg.lovelaceConfig != null) {
-    lovelace.mode = "yaml";
-  };
-
-in {
-  meta.maintainers = teams.home-assistant.members;
-
-  options.services.home-assistant = {
-    # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
-    # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
-    enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
-
-    configDir = mkOption {
-      default = "/var/lib/hass";
-      type = types.path;
-      description = "The config directory, where your <filename>configuration.yaml</filename> is located.";
-    };
-
-    port = mkOption {
-      default = 8123;
-      type = types.port;
-      description = "The port on which to listen.";
-    };
-
-    applyDefaultConfig = mkOption {
-      default = true;
-      type = types.bool;
-      description = ''
-        Setting this option enables a few configuration options for HA based on NixOS configuration (such as time zone) to avoid having to manually specify configuration we already have.
-        </para>
-        <para>
-        Currently one side effect of enabling this is that the <literal>http</literal> component will be enabled.
-        </para>
-        <para>
-        This only takes effect if <literal>config != null</literal> in order to ensure that a manually managed <filename>configuration.yaml</filename> is not overwritten.
-      '';
-    };
-
-    config = mkOption {
-      default = null;
-      # Migrate to new option types later: https://github.com/NixOS/nixpkgs/pull/75584
-      type =  with lib.types; let
-          valueType = nullOr (oneOf [
-            bool
-            int
-            float
-            str
-            (lazyAttrsOf valueType)
-            (listOf valueType)
-          ]) // {
-            description = "Yaml value";
-            emptyValue.value = {};
-          };
-        in valueType;
-      example = literalExpression ''
-        {
-          homeassistant = {
-            name = "Home";
-            latitude = "!secret latitude";
-            longitude = "!secret longitude";
-            elevation = "!secret elevation";
-            unit_system = "metric";
-            time_zone = "UTC";
-          };
-          frontend = {
-            themes = "!include_dir_merge_named themes";
-          };
-          http = { };
-          feedreader.urls = [ "https://nixos.org/blogs.xml" ];
-        }
-      '';
-      description = ''
-        Your <filename>configuration.yaml</filename> as a Nix attribute set.
-        Beware that setting this option will delete your previous <filename>configuration.yaml</filename>.
-        <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">Secrets</link>
-        are encoded as strings as shown in the example.
-      '';
-    };
-
-    configWritable = mkOption {
-      default = false;
-      type = types.bool;
-      description = ''
-        Whether to make <filename>configuration.yaml</filename> writable.
-        This only has an effect if <option>config</option> is set.
-        This will allow you to edit it from Home Assistant's web interface.
-        However, bear in mind that it will be overwritten at every start of the service.
-      '';
-    };
-
-    lovelaceConfig = mkOption {
-      default = null;
-      type = with types; nullOr attrs;
-      # from https://www.home-assistant.io/lovelace/yaml-mode/
-      example = literalExpression ''
-        {
-          title = "My Awesome Home";
-          views = [ {
-            title = "Example";
-            cards = [ {
-              type = "markdown";
-              title = "Lovelace";
-              content = "Welcome to your **Lovelace UI**.";
-            } ];
-          } ];
-        }
-      '';
-      description = ''
-        Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
-        Setting this option will automatically add
-        <literal>lovelace.mode = "yaml";</literal> to your <option>config</option>.
-        Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
-      '';
-    };
-
-    lovelaceConfigWritable = mkOption {
-      default = false;
-      type = types.bool;
-      description = ''
-        Whether to make <filename>ui-lovelace.yaml</filename> writable.
-        This only has an effect if <option>lovelaceConfig</option> is set.
-        This will allow you to edit it from Home Assistant's web interface.
-        However, bear in mind that it will be overwritten at every start of the service.
-      '';
-    };
-
-    package = mkOption {
-      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
-        doInstallCheck = false;
-      });
-      defaultText = literalExpression ''
-        pkgs.home-assistant.overrideAttrs (oldAttrs: {
-          doInstallCheck = false;
-        })
-      '';
-      type = types.package;
-      example = literalExpression ''
-        pkgs.home-assistant.override {
-          extraPackages = ps: with ps; [ colorlog ];
-        }
-      '';
-      description = ''
-        Home Assistant package to use. By default the tests are disabled, as they take a considerable amout of time to complete.
-        Override <literal>extraPackages</literal> or <literal>extraComponents</literal> in order to add additional dependencies.
-        If you specify <option>config</option> and do not set <option>autoExtraComponents</option>
-        to <literal>false</literal>, overriding <literal>extraComponents</literal> will have no effect.
-        Avoid <literal>home-assistant.overridePythonAttrs</literal> if you use <literal>autoExtraComponents</literal>.
-      '';
-    };
-
-    autoExtraComponents = mkOption {
-      default = true;
-      type = types.bool;
-      description = ''
-        If set to <literal>true</literal>, the components used in <literal>config</literal>
-        are set as the specified package's <literal>extraComponents</literal>.
-        This in turn adds all packaged dependencies to the derivation.
-        You might still see import errors in your log.
-        In this case, you will need to package the necessary dependencies yourself
-        or ask for someone else to package them.
-        If a dependency is packaged but not automatically added to this list,
-        you might need to specify it in <literal>extraPackages</literal>.
-      '';
-    };
-
-    openFirewall = mkOption {
-      default = false;
-      type = types.bool;
-      description = "Whether to open the firewall for the specified port.";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
-
-    systemd.services.home-assistant = {
-      description = "Home Assistant";
-      after = [ "network.target" ];
-      preStart = optionalString (cfg.config != null) (if cfg.configWritable then ''
-        cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
-      '' else ''
-        rm -f "${cfg.configDir}/configuration.yaml"
-        ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
-      '') + optionalString (cfg.lovelaceConfig != null) (if cfg.lovelaceConfigWritable then ''
-        cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
-      '' else ''
-        rm -f "${cfg.configDir}/ui-lovelace.yaml"
-        ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
-      '');
-      serviceConfig = let
-        # List of capabilities to equip home-assistant with, depending on configured components
-        capabilities = [
-          # Empty string first, so we will never accidentally have an empty capability bounding set
-          # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
-          ""
-        ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
-          # Required for interaction with hci devices and bluetooth sockets
-          # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
-          "CAP_NET_ADMIN"
-          "CAP_NET_RAW"
-        ] ++ lib.optionals (useComponent "emulated_hue") [
-          # Alexa looks for the service on port 80
-          # https://www.home-assistant.io/integrations/emulated_hue
-          "CAP_NET_BIND_SERVICE"
-        ] ++ lib.optionals (useComponent "nmap_tracker") [
-          # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
-          "CAP_NET_ADMIN"
-          "CAP_NET_BIND_SERVICE"
-          "CAP_NET_RAW"
-        ]));
-        componentsUsingBluetooth = [
-          # Components that require the AF_BLUETOOTH address family
-          "bluetooth_tracker"
-          "bluetooth_le_tracker"
-        ];
-        componentsUsingPing = [
-          # Components that require the capset syscall for the ping wrapper
-          "ping"
-          "wake_on_lan"
-        ];
-        componentsUsingSerialDevices = [
-          # Components that require access to serial devices (/dev/tty*)
-          # List generated from home-assistant documentation:
-          #   git clone https://github.com/home-assistant/home-assistant.io/
-          #   cd source/_integrations
-          #   rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
-          # And then extended by references found in the source code, these
-          # mostly the ones using config flows already.
-          "acer_projector"
-          "alarmdecoder"
-          "arduino"
-          "blackbird"
-          "deconz"
-          "dsmr"
-          "edl21"
-          "elkm1"
-          "elv"
-          "enocean"
-          "firmata"
-          "flexit"
-          "gpsd"
-          "insteon"
-          "kwb"
-          "lacrosse"
-          "mhz19"
-          "modbus"
-          "modem_callerid"
-          "mysensors"
-          "nad"
-          "numato"
-          "rflink"
-          "rfxtrx"
-          "scsgate"
-          "serial"
-          "serial_pm"
-          "sms"
-          "upb"
-          "usb"
-          "velbus"
-          "w800rf32"
-          "xbee"
-          "zha"
-          "zwave"
-          "zwave_js"
-        ];
-      in {
-        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        User = "hass";
-        Group = "hass";
-        Restart = "on-failure";
-        RestartForceExitStatus = "100";
-        SuccessExitStatus = "100";
-        KillSignal = "SIGINT";
-
-        # Hardening
-        AmbientCapabilities = capabilities;
-        CapabilityBoundingSet = capabilities;
-        DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
-          "char-ttyACM rw"
-          "char-ttyAMA rw"
-          "char-ttyUSB rw"
-        ]);
-        DevicePolicy = "closed";
-        LockPersonality = true;
-        MemoryDenyWriteExecute = true;
-        NoNewPrivileges = true;
-        PrivateTmp = true;
-        PrivateUsers = false; # prevents gaining capabilities in the host namespace
-        ProtectClock = true;
-        ProtectControlGroups = true;
-        ProtectHome = true;
-        ProtectHostname = true;
-        ProtectKernelLogs = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        ProtectProc = "invisible";
-        ProcSubset = "all";
-        ProtectSystem = "strict";
-        RemoveIPC = true;
-        ReadWritePaths = let
-          # Allow rw access to explicitly configured paths
-          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
-          value = attrByPath cfgPath [] cfg;
-          allowPaths = if isList value then value else singleton value;
-        in [ "${cfg.configDir}" ] ++ allowPaths;
-        RestrictAddressFamilies = [
-          "AF_INET"
-          "AF_INET6"
-          "AF_NETLINK"
-          "AF_UNIX"
-        ] ++ optionals (any useComponent componentsUsingBluetooth) [
-          "AF_BLUETOOTH"
-        ];
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        RestrictSUIDSGID = true;
-        SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
-          "dialout"
-        ];
-        SystemCallArchitectures = "native";
-        SystemCallFilter = [
-          "@system-service"
-          "~@privileged"
-        ] ++ optionals (any useComponent componentsUsingPing) [
-          "capset"
-        ];
-        UMask = "0077";
-      };
-      path = [
-        "/run/wrappers" # needed for ping
-      ];
-    };
-
-    systemd.targets.home-assistant = rec {
-      description = "Home Assistant";
-      wantedBy = [ "multi-user.target" ];
-      wants = [ "home-assistant.service" ];
-      after = wants;
-    };
-
-    users.users.hass = {
-      home = cfg.configDir;
-      createHome = true;
-      group = "hass";
-      uid = config.ids.uids.hass;
-    };
-
-    users.groups.hass.gid = config.ids.gids.hass;
-  };
-}
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
new file mode 100644
index 0000000000000..f5fb2bf53086f
--- /dev/null
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -0,0 +1,30 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let cfg = config.services.input-remapper; in
+{
+  options = {
+    services.input-remapper = {
+      enable = mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons.";
+      package = options.mkPackageOption pkgs "input-remapper" { };
+      enableUdevRules = mkEnableOption "udev rules added by input-remapper to handle hotplugged devices. Currently disabled by default due to https://github.com/sezanzeb/input-remapper/issues/140";
+      serviceWantedBy = mkOption {
+        default = [ "graphical.target" ];
+        example = [ "multi-user.target" ];
+        type = types.listOf types.str;
+        description = "Specifies the WantedBy setting for the input-remapper service.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = mkIf cfg.enableUdevRules [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package ];
+    systemd.services.input-remapper.wantedBy = cfg.serviceWantedBy;
+  };
+
+  meta.maintainers = with lib.maintainers; [ LunNova ];
+}
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index b9d54f27edc21..e2f227b06196a 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -49,51 +49,61 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      # This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service
+      # Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option
       serviceConfig = rec {
+        Type = "simple";
         User = cfg.user;
         Group = cfg.group;
         StateDirectory = "jellyfin";
+        StateDirectoryMode = "0700";
         CacheDirectory = "jellyfin";
+        CacheDirectoryMode = "0700";
+        UMask = "0077";
+        WorkingDirectory = "/var/lib/jellyfin";
         ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
         Restart = "on-failure";
+        TimeoutSec = 15;
+        SuccessExitStatus = ["0" "143"];
 
         # 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;
+        SystemCallArchitectures = "native";
         # AF_NETLINK needed because Jellyfin monitors the network connection
-        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" ];
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictNamespaces = !config.boot.isContainer;
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
+        ProtectControlGroups = !config.boot.isContainer;
+        ProtectHostname = true;
+        ProtectKernelLogs = !config.boot.isContainer;
+        ProtectKernelModules = !config.boot.isContainer;
+        ProtectKernelTunables = !config.boot.isContainer;
+        LockPersonality = true;
+        PrivateTmp = !config.boot.isContainer;
+        # needed for hardware accelaration
+        PrivateDevices = false;
+        PrivateUsers = true;
+        RemoveIPC = true;
 
-        SystemCallArchitectures = "native";
-        SystemCallErrorNumber = "EPERM";
         SystemCallFilter = [
-          "@system-service"
-          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+          "~@clock"
+          "~@aio"
+          "~@chown"
+          "~@cpu-emulation"
+          "~@debug"
+          "~@keyring"
+          "~@memlock"
+          "~@module"
+          "~@mount"
+          "~@obsolete"
+          "~@privileged"
+          "~@raw-io"
+          "~@reboot"
+          "~@setuid"
+          "~@swap"
         ];
+        SystemCallErrorNumber = "EPERM";
       };
     };
 
diff --git a/nixos/modules/services/misc/libreddit.nix b/nixos/modules/services/misc/libreddit.nix
index 77b34a8562049..e21a884478441 100644
--- a/nixos/modules/services/misc/libreddit.nix
+++ b/nixos/modules/services/misc/libreddit.nix
@@ -2,14 +2,13 @@
 
 with lib;
 
-  let
-    cfg = config.services.libreddit;
-
-    args = concatStringsSep " " ([
-      "--port ${toString cfg.port}"
-      "--address ${cfg.address}"
-    ] ++ optional cfg.redirect "--redirect-https");
+let
+  cfg = config.services.libreddit;
 
+  args = concatStringsSep " " ([
+    "--port ${toString cfg.port}"
+    "--address ${cfg.address}"
+  ]);
 in
 {
   options = {
@@ -30,12 +29,6 @@ in
         description = "The port to listen on";
       };
 
-      redirect = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Enable the redirecting to HTTPS";
-      };
-
       openFirewall = mkOption {
         type = types.bool;
         default = false;
@@ -56,6 +49,31 @@ in
           AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
           Restart = "on-failure";
           RestartSec = "2s";
+          # Hardening
+          CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          # A private user cannot have process capabilities on the host's user
+          # namespace and thus CAP_NET_BIND_SERVICE has no effect.
+          PrivateUsers = (cfg.port >= 1024);
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
         };
     };
 
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
deleted file mode 100644
index feca4c5465ff5..0000000000000
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ /dev/null
@@ -1,844 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.matrix-synapse;
-  opt = options.services.matrix-synapse;
-  pg = config.services.postgresql;
-  usePostgresql = cfg.database_type == "psycopg2";
-  logConfigFile = pkgs.writeText "log_config.yaml" cfg.logConfig;
-  mkResource = r: ''{names: ${builtins.toJSON r.names}, compress: ${boolToString r.compress}}'';
-  mkListener = l: ''{port: ${toString l.port}, bind_address: "${l.bind_address}", type: ${l.type}, tls: ${boolToString l.tls}, x_forwarded: ${boolToString l.x_forwarded}, resources: [${concatStringsSep "," (map mkResource l.resources)}]}'';
-  pluginsEnv = cfg.package.python.buildEnv.override {
-    extraLibs = cfg.plugins;
-  };
-  configFile = pkgs.writeText "homeserver.yaml" ''
-${optionalString (cfg.tls_certificate_path != null) ''
-tls_certificate_path: "${cfg.tls_certificate_path}"
-''}
-${optionalString (cfg.tls_private_key_path != null) ''
-tls_private_key_path: "${cfg.tls_private_key_path}"
-''}
-${optionalString (cfg.tls_dh_params_path != null) ''
-tls_dh_params_path: "${cfg.tls_dh_params_path}"
-''}
-no_tls: ${boolToString cfg.no_tls}
-${optionalString (cfg.bind_port != null) ''
-bind_port: ${toString cfg.bind_port}
-''}
-${optionalString (cfg.unsecure_port != null) ''
-unsecure_port: ${toString cfg.unsecure_port}
-''}
-${optionalString (cfg.bind_host != null) ''
-bind_host: "${cfg.bind_host}"
-''}
-server_name: "${cfg.server_name}"
-pid_file: "/run/matrix-synapse.pid"
-${optionalString (cfg.public_baseurl != null) ''
-public_baseurl: "${cfg.public_baseurl}"
-''}
-listeners: [${concatStringsSep "," (map mkListener cfg.listeners)}]
-database: {
-  name: "${cfg.database_type}",
-  args: {
-    ${concatStringsSep ",\n    " (
-      mapAttrsToList (n: v: "\"${n}\": ${builtins.toJSON v}") cfg.database_args
-    )}
-  }
-}
-event_cache_size: "${cfg.event_cache_size}"
-verbose: ${cfg.verbose}
-log_config: "${logConfigFile}"
-rc_messages_per_second: ${cfg.rc_messages_per_second}
-rc_message_burst_count: ${cfg.rc_message_burst_count}
-federation_rc_window_size: ${cfg.federation_rc_window_size}
-federation_rc_sleep_limit: ${cfg.federation_rc_sleep_limit}
-federation_rc_sleep_delay: ${cfg.federation_rc_sleep_delay}
-federation_rc_reject_limit: ${cfg.federation_rc_reject_limit}
-federation_rc_concurrent: ${cfg.federation_rc_concurrent}
-media_store_path: "${cfg.dataDir}/media"
-uploads_path: "${cfg.dataDir}/uploads"
-max_upload_size: "${cfg.max_upload_size}"
-max_image_pixels: "${cfg.max_image_pixels}"
-dynamic_thumbnails: ${boolToString cfg.dynamic_thumbnails}
-url_preview_enabled: ${boolToString cfg.url_preview_enabled}
-${optionalString (cfg.url_preview_enabled == true) ''
-url_preview_ip_range_blacklist: ${builtins.toJSON cfg.url_preview_ip_range_blacklist}
-url_preview_ip_range_whitelist: ${builtins.toJSON cfg.url_preview_ip_range_whitelist}
-url_preview_url_blacklist: ${builtins.toJSON cfg.url_preview_url_blacklist}
-''}
-recaptcha_private_key: "${cfg.recaptcha_private_key}"
-recaptcha_public_key: "${cfg.recaptcha_public_key}"
-enable_registration_captcha: ${boolToString cfg.enable_registration_captcha}
-turn_uris: ${builtins.toJSON cfg.turn_uris}
-turn_shared_secret: "${cfg.turn_shared_secret}"
-enable_registration: ${boolToString cfg.enable_registration}
-${optionalString (cfg.registration_shared_secret != null) ''
-registration_shared_secret: "${cfg.registration_shared_secret}"
-''}
-recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify"
-turn_user_lifetime: "${cfg.turn_user_lifetime}"
-user_creation_max_duration: ${cfg.user_creation_max_duration}
-bcrypt_rounds: ${cfg.bcrypt_rounds}
-allow_guest_access: ${boolToString cfg.allow_guest_access}
-
-account_threepid_delegates:
-  ${optionalString (cfg.account_threepid_delegates.email != null) "email: ${cfg.account_threepid_delegates.email}"}
-  ${optionalString (cfg.account_threepid_delegates.msisdn != null) "msisdn: ${cfg.account_threepid_delegates.msisdn}"}
-
-room_prejoin_state:
-  disable_default_event_types: ${boolToString cfg.room_prejoin_state.disable_default_event_types}
-  additional_event_types: ${builtins.toJSON cfg.room_prejoin_state.additional_event_types}
-${optionalString (cfg.macaroon_secret_key != null) ''
-  macaroon_secret_key: "${cfg.macaroon_secret_key}"
-''}
-expire_access_token: ${boolToString cfg.expire_access_token}
-enable_metrics: ${boolToString cfg.enable_metrics}
-report_stats: ${boolToString cfg.report_stats}
-signing_key_path: "${cfg.dataDir}/homeserver.signing.key"
-key_refresh_interval: "${cfg.key_refresh_interval}"
-perspectives:
-  servers: {
-    ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
-    "${n}": {
-      "verify_keys": {
-        ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
-        "${n}": {
-          "key": "${v}"
-        }'') v)}
-      }
-    '') cfg.servers)}
-    }
-  }
-redaction_retention_period: ${toString cfg.redaction_retention_period}
-app_service_config_files: ${builtins.toJSON cfg.app_service_config_files}
-
-${cfg.extraConfig}
-'';
-
-  hasLocalPostgresDB = let args = cfg.database_args; in
-    usePostgresql && (!(args ? host) || (elem args.host [ "localhost" "127.0.0.1" "::1" ]));
-
-  registerNewMatrixUser =
-    let
-      isIpv6 = x: lib.length (lib.splitString ":" x) > 1;
-      listener =
-        lib.findFirst (
-          listener: lib.any (
-            resource: lib.any (
-              name: name == "client"
-            ) resource.names
-          ) listener.resources
-        ) (lib.last cfg.listeners) cfg.listeners;
-    in
-    pkgs.writeShellScriptBin "matrix-synapse-register_new_matrix_user" ''
-      exec ${cfg.package}/bin/register_new_matrix_user \
-        $@ \
-        ${lib.concatMapStringsSep " " (x: "-c ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} \
-        "${listener.type}://${
-          if (isIpv6 listener.bind_address) then
-            "[${listener.bind_address}]"
-          else
-            "${listener.bind_address}"
-        }:${builtins.toString listener.port}/"
-    '';
-in {
-  options = {
-    services.matrix-synapse = {
-      enable = mkEnableOption "matrix.org synapse";
-      configFile = mkOption {
-        type = types.str;
-        readOnly = true;
-        description = ''
-          Path to the configuration file on the target system. Useful to configure e.g. workers
-          that also need this.
-        '';
-      };
-      package = mkOption {
-        type = types.package;
-        default = pkgs.matrix-synapse;
-        defaultText = literalExpression "pkgs.matrix-synapse";
-        description = ''
-          Overridable attribute of the matrix synapse server package to use.
-        '';
-      };
-      plugins = mkOption {
-        type = types.listOf types.package;
-        default = [ ];
-        example = literalExpression ''
-          with config.services.matrix-synapse.package.plugins; [
-            matrix-synapse-ldap3
-            matrix-synapse-pam
-          ];
-        '';
-        description = ''
-          List of additional Matrix plugins to make available.
-        '';
-      };
-      withJemalloc = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to preload jemalloc to reduce memory fragmentation and overall usage.
-        '';
-      };
-      no_tls = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Don't bind to the https port
-        '';
-      };
-      bind_port = mkOption {
-        type = types.nullOr types.int;
-        default = null;
-        example = 8448;
-        description = ''
-          DEPRECATED: Use listeners instead.
-          The port to listen for HTTPS requests on.
-          For when matrix traffic is sent directly to synapse.
-        '';
-      };
-      unsecure_port = mkOption {
-        type = types.nullOr types.int;
-        default = null;
-        example = 8008;
-        description = ''
-          DEPRECATED: Use listeners instead.
-          The port to listen for HTTP requests on.
-          For when matrix traffic passes through loadbalancer that unwraps TLS.
-        '';
-      };
-      bind_host = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          DEPRECATED: Use listeners instead.
-          Local interface to listen on.
-          The empty string will cause synapse to listen on all interfaces.
-        '';
-      };
-      tls_certificate_path = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/matrix-synapse/homeserver.tls.crt";
-        description = ''
-          PEM encoded X509 certificate for TLS.
-          You can replace the self-signed certificate that synapse
-          autogenerates on launch with your own SSL certificate + key pair
-          if you like.  Any required intermediary certificates can be
-          appended after the primary certificate in hierarchical order.
-        '';
-      };
-      tls_private_key_path = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/matrix-synapse/homeserver.tls.key";
-        description = ''
-          PEM encoded private key for TLS. Specify null if synapse is not
-          speaking TLS directly.
-        '';
-      };
-      tls_dh_params_path = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/matrix-synapse/homeserver.tls.dh";
-        description = ''
-          PEM dh parameters for ephemeral keys
-        '';
-      };
-      server_name = mkOption {
-        type = types.str;
-        example = "example.com";
-        default = config.networking.hostName;
-        defaultText = literalExpression "config.networking.hostName";
-        description = ''
-          The domain name of the server, with optional explicit port.
-          This is used by remote servers to look up the server address.
-          This is also the last part of your UserID.
-
-          The server_name cannot be changed later so it is important to configure this correctly before you start Synapse.
-        '';
-      };
-      public_baseurl = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "https://example.com:8448/";
-        description = ''
-          The public-facing base URL for the client API (not including _matrix/...)
-        '';
-      };
-      listeners = mkOption {
-        type = types.listOf (types.submodule {
-          options = {
-            port = mkOption {
-              type = types.port;
-              example = 8448;
-              description = ''
-                The port to listen for HTTP(S) requests on.
-              '';
-            };
-            bind_address = mkOption {
-              type = types.str;
-              default = "";
-              example = "203.0.113.42";
-              description = ''
-                Local interface to listen on.
-                The empty string will cause synapse to listen on all interfaces.
-              '';
-            };
-            type = mkOption {
-              type = types.str;
-              default = "http";
-              description = ''
-                Type of listener.
-              '';
-            };
-            tls = mkOption {
-              type = types.bool;
-              default = true;
-              description = ''
-                Whether to listen for HTTPS connections rather than HTTP.
-              '';
-            };
-            x_forwarded = mkOption {
-              type = types.bool;
-              default = false;
-              description = ''
-                Use the X-Forwarded-For (XFF) header as the client IP and not the
-                actual client IP.
-              '';
-            };
-            resources = mkOption {
-              type = types.listOf (types.submodule {
-                options = {
-                  names = mkOption {
-                    type = types.listOf types.str;
-                    description = ''
-                      List of resources to host on this listener.
-                    '';
-                    example = ["client" "federation"];
-                  };
-                  compress = mkOption {
-                    type = types.bool;
-                    description = ''
-                      Should synapse compress HTTP responses to clients that support it?
-                      This should be disabled if running synapse behind a load balancer
-                      that can do automatic compression.
-                    '';
-                  };
-                };
-              });
-              description = ''
-                List of HTTP resources to serve on this listener.
-              '';
-            };
-          };
-        });
-        default = [{
-          port = 8448;
-          bind_address = "";
-          type = "http";
-          tls = true;
-          x_forwarded = false;
-          resources = [
-            { names = ["client"]; compress = true; }
-            { names = ["federation"]; compress = false; }
-          ];
-        }];
-        description = ''
-          List of ports that Synapse should listen on, their purpose and their configuration.
-        '';
-      };
-      verbose = mkOption {
-        type = types.str;
-        default = "0";
-        description = "Logging verbosity level.";
-      };
-      rc_messages_per_second = mkOption {
-        type = types.str;
-        default = "0.2";
-        description = "Number of messages a client can send per second";
-      };
-      rc_message_burst_count = mkOption {
-        type = types.str;
-        default = "10.0";
-        description = "Number of message a client can send before being throttled";
-      };
-      federation_rc_window_size = mkOption {
-        type = types.str;
-        default = "1000";
-        description = "The federation window size in milliseconds";
-      };
-      federation_rc_sleep_limit = mkOption {
-        type = types.str;
-        default = "10";
-        description = ''
-          The number of federation requests from a single server in a window
-          before the server will delay processing the request.
-        '';
-      };
-      federation_rc_sleep_delay = mkOption {
-        type = types.str;
-        default = "500";
-        description = ''
-          The duration in milliseconds to delay processing events from
-          remote servers by if they go over the sleep limit.
-        '';
-      };
-      federation_rc_reject_limit = mkOption {
-        type = types.str;
-        default = "50";
-        description = ''
-          The maximum number of concurrent federation requests allowed
-          from a single server
-        '';
-      };
-      federation_rc_concurrent = mkOption {
-        type = types.str;
-        default = "3";
-        description = "The number of federation requests to concurrently process from a single server";
-      };
-      database_type = mkOption {
-        type = types.enum [ "sqlite3" "psycopg2" ];
-        default = if versionAtLeast config.system.stateVersion "18.03"
-          then "psycopg2"
-          else "sqlite3";
-        defaultText = literalExpression ''
-          if versionAtLeast config.system.stateVersion "18.03"
-            then "psycopg2"
-            else "sqlite3"
-        '';
-        description = ''
-          The database engine name. Can be sqlite or psycopg2.
-        '';
-      };
-      database_name = mkOption {
-        type = types.str;
-        default = "matrix-synapse";
-        description = "Database name.";
-      };
-      database_user = mkOption {
-        type = types.str;
-        default = "matrix-synapse";
-        description = "Database user name.";
-      };
-      database_args = mkOption {
-        type = types.attrs;
-        default = {
-          sqlite3 = { database = "${cfg.dataDir}/homeserver.db"; };
-          psycopg2 = {
-            user = cfg.database_user;
-            database = cfg.database_name;
-          };
-        }.${cfg.database_type};
-        defaultText = literalDocBook ''
-          <variablelist>
-            <varlistentry>
-              <term>using sqlite3</term>
-              <listitem>
-                <programlisting>
-                  { database = "''${config.${opt.dataDir}}/homeserver.db"; }
-                </programlisting>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>using psycopg2</term>
-              <listitem>
-                <programlisting>
-                  psycopg2 = {
-                    user = config.${opt.database_user};
-                    database = config.${opt.database_name};
-                  }
-                </programlisting>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        '';
-        description = ''
-          Arguments to pass to the engine.
-        '';
-      };
-      event_cache_size = mkOption {
-        type = types.str;
-        default = "10K";
-        description = "Number of events to cache in memory.";
-      };
-      url_preview_enabled = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Is the preview URL API enabled?  If enabled, you *must* specify an
-          explicit url_preview_ip_range_blacklist of IPs that the spider is
-          denied from accessing.
-        '';
-      };
-      url_preview_ip_range_blacklist = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "127.0.0.0/8"
-          "10.0.0.0/8"
-          "172.16.0.0/12"
-          "192.168.0.0/16"
-          "100.64.0.0/10"
-          "169.254.0.0/16"
-          "::1/128"
-          "fe80::/64"
-          "fc00::/7"
-        ];
-        description = ''
-          List of IP address CIDR ranges that the URL preview spider is denied
-          from accessing.
-        '';
-      };
-      url_preview_ip_range_whitelist = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = ''
-          List of IP address CIDR ranges that the URL preview spider is allowed
-          to access even if they are specified in
-          url_preview_ip_range_blacklist.
-        '';
-      };
-      url_preview_url_blacklist = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = ''
-          Optional list of URL matches that the URL preview spider is
-          denied from accessing.
-        '';
-      };
-      recaptcha_private_key = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          This Home Server's ReCAPTCHA private key.
-        '';
-      };
-      recaptcha_public_key = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          This Home Server's ReCAPTCHA public key.
-        '';
-      };
-      enable_registration_captcha = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enables ReCaptcha checks when registering, preventing signup
-          unless a captcha is answered. Requires a valid ReCaptcha
-          public/private key.
-        '';
-      };
-      turn_uris = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = ''
-          The public URIs of the TURN server to give to clients
-        '';
-      };
-      turn_shared_secret = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          The shared secret used to compute passwords for the TURN server
-        '';
-      };
-      turn_user_lifetime = mkOption {
-        type = types.str;
-        default = "1h";
-        description = "How long generated TURN credentials last";
-      };
-      enable_registration = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable registration for new users.
-        '';
-      };
-      registration_shared_secret = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          If set, allows registration by anyone who also has the shared
-          secret, even if registration is otherwise disabled.
-        '';
-      };
-      enable_metrics = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable collection and rendering of performance metrics
-        '';
-      };
-      report_stats = mkOption {
-        type = types.bool;
-        default = false;
-        description = "";
-      };
-      servers = mkOption {
-        type = types.attrsOf (types.attrsOf types.str);
-        default = {
-          "matrix.org" = {
-            "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
-          };
-        };
-        description = ''
-          The trusted servers to download signing keys from.
-        '';
-      };
-      max_upload_size = mkOption {
-        type = types.str;
-        default = "10M";
-        description = "The largest allowed upload size in bytes";
-      };
-      max_image_pixels = mkOption {
-        type = types.str;
-        default = "32M";
-        description = "Maximum number of pixels that will be thumbnailed";
-      };
-      dynamic_thumbnails = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to generate new thumbnails on the fly to precisely match
-          the resolution requested by the client. If true then whenever
-          a new resolution is requested by the client the server will
-          generate a new thumbnail. If false the server will pick a thumbnail
-          from a precalculated list.
-        '';
-      };
-      user_creation_max_duration = mkOption {
-        type = types.str;
-        default = "1209600000";
-        description = ''
-          Sets the expiry for the short term user creation in
-          milliseconds. The default value is two weeks.
-        '';
-      };
-      bcrypt_rounds = mkOption {
-        type = types.str;
-        default = "12";
-        description = ''
-          Set the number of bcrypt rounds used to generate password hash.
-          Larger numbers increase the work factor needed to generate the hash.
-        '';
-      };
-      allow_guest_access = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Allows users to register as guests without a password/email/etc, and
-          participate in rooms hosted on this server which have been made
-          accessible to anonymous users.
-        '';
-      };
-      account_threepid_delegates.email = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Delegate email sending to https://example.org
-        '';
-      };
-      account_threepid_delegates.msisdn = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Delegate SMS sending to this local process (https://localhost:8090)
-        '';
-      };
-      room_prejoin_state.additional_event_types = mkOption {
-        default = [];
-        type = types.listOf types.str;
-        description = ''
-          Additional events to share with users who received an invite.
-        '';
-      };
-      room_prejoin_state.disable_default_event_types = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Whether to disable the default state-event types for users invited to a room.
-          These are:
-
-          <itemizedlist>
-          <listitem><para>m.room.join_rules</para></listitem>
-          <listitem><para>m.room.canonical_alias</para></listitem>
-          <listitem><para>m.room.avatar</para></listitem>
-          <listitem><para>m.room.encryption</para></listitem>
-          <listitem><para>m.room.name</para></listitem>
-          <listitem><para>m.room.create</para></listitem>
-          </itemizedlist>
-        '';
-      };
-      macaroon_secret_key = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Secret key for authentication tokens
-        '';
-      };
-      expire_access_token = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable access token expiration.
-        '';
-      };
-      key_refresh_interval = mkOption {
-        type = types.str;
-        default = "1d";
-        description = ''
-          How long key response published by this server is valid for.
-          Used to set the valid_until_ts in /key/v2 APIs.
-          Determines how quickly servers will query to check which keys
-          are still valid.
-        '';
-      };
-      app_service_config_files = mkOption {
-        type = types.listOf types.path;
-        default = [ ];
-        description = ''
-          A list of application service config file to use
-        '';
-      };
-      redaction_retention_period = mkOption {
-        type = types.int;
-        default = 7;
-        description = ''
-          How long to keep redacted events in unredacted form in the database.
-        '';
-      };
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Extra config options for matrix-synapse.
-        '';
-      };
-      extraConfigFiles = mkOption {
-        type = types.listOf types.path;
-        default = [];
-        description = ''
-          Extra config files to include.
-
-          The configuration files will be included based on the command line
-          argument --config-path. This allows to configure secrets without
-          having to go through the Nix store, e.g. based on deployment keys if
-          NixOPS is in use.
-        '';
-      };
-      logConfig = mkOption {
-        type = types.lines;
-        default = readFile ./matrix-synapse-log_config.yaml;
-        description = ''
-          A yaml python logging config file
-        '';
-      };
-      dataDir = mkOption {
-        type = types.str;
-        default = "/var/lib/matrix-synapse";
-        description = ''
-          The directory where matrix-synapse stores its stateful data such as
-          certificates, media and uploads.
-        '';
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    assertions = [
-      { assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
-        message = ''
-          Cannot deploy matrix-synapse with a configuration for a local postgresql database
-            and a missing postgresql service. Since 20.03 it's mandatory to manually configure the
-            database (please read the thread in https://github.com/NixOS/nixpkgs/pull/80447 for
-            further reference).
-
-            If you
-            - try to deploy a fresh synapse, you need to configure the database yourself. An example
-              for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix>
-            - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true`
-              to your configuration.
-
-          For further information about this update, please read the release-notes of 20.03 carefully.
-        '';
-      }
-    ];
-
-    services.matrix-synapse.configFile = "${configFile}";
-
-    users.users.matrix-synapse = {
-      group = "matrix-synapse";
-      home = cfg.dataDir;
-      createHome = true;
-      shell = "${pkgs.bash}/bin/bash";
-      uid = config.ids.uids.matrix-synapse;
-    };
-
-    users.groups.matrix-synapse = {
-      gid = config.ids.gids.matrix-synapse;
-    };
-
-    systemd.services.matrix-synapse = {
-      description = "Synapse Matrix homeserver";
-      after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        ${cfg.package}/bin/synapse_homeserver \
-          --config-path ${configFile} \
-          --keys-directory ${cfg.dataDir} \
-          --generate-keys
-      '';
-      environment = {
-        PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
-      } // optionalAttrs (cfg.withJemalloc) {
-        LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
-      };
-      serviceConfig = {
-        Type = "notify";
-        User = "matrix-synapse";
-        Group = "matrix-synapse";
-        WorkingDirectory = cfg.dataDir;
-        ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
-          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
-          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
-        '')) ];
-        ExecStart = ''
-          ${cfg.package}/bin/synapse_homeserver \
-            ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
-            --keys-directory ${cfg.dataDir}
-        '';
-        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
-        Restart = "on-failure";
-        UMask = "0077";
-      };
-    };
-
-    environment.systemPackages = [ registerNewMatrixUser ];
-  };
-
-  imports = [
-    (mkRemovedOptionModule [ "services" "matrix-synapse" "trusted_third_party_id_servers" ] ''
-      The `trusted_third_party_id_servers` option as been removed in `matrix-synapse` v1.4.0
-      as the behavior is now obsolete.
-    '')
-    (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] ''
-      Database configuration must be done manually. An exemplary setup is demonstrated in
-      <nixpkgs/nixos/tests/matrix-synapse.nix>
-    '')
-    (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
-    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
-      You may add additional event types via
-      `services.matrix-synapse.room_prejoin_state.additional_event_types` and
-      disable the default events via
-      `services.matrix-synapse.room_prejoin_state.disable_default_event_types`.
-    '')
-  ];
-
-  meta.doc = ./matrix-synapse.xml;
-  meta.maintainers = teams.matrix.members;
-
-}
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
deleted file mode 100644
index 41a56df0f2b5e..0000000000000
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ /dev/null
@@ -1,230 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-matrix">
- <title>Matrix</title>
- <para>
-  <link xlink:href="https://matrix.org/">Matrix</link> is an open standard for
-  interoperable, decentralised, real-time communication over IP. It can be used
-  to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
-  communication - or anywhere you need a standard HTTP API for publishing and
-  subscribing to data whilst tracking the conversation history.
- </para>
- <para>
-  This chapter will show you how to set up your own, self-hosted Matrix
-  homeserver using the Synapse reference homeserver, and how to serve your own
-  copy of the Element web client. See the
-  <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
-  Matrix Now!</link> overview page for links to Element Apps for Android and iOS,
-  desktop clients, as well as bridges to other networks and other projects
-  around Matrix.
- </para>
- <section xml:id="module-services-matrix-synapse">
-  <title>Synapse Homeserver</title>
-
-  <para>
-   <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link> is
-   the reference homeserver implementation of Matrix from the core development
-   team at matrix.org. The following configuration example will set up a
-   synapse server for the <literal>example.org</literal> domain, served from
-   the host <literal>myhostname.example.org</literal>. For more information,
-   please refer to the
-   <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
-   installation instructions of Synapse </link>.
-<programlisting>
-{ pkgs, lib, ... }:
-let
-  fqdn =
-    let
-      join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}";
-    in join config.networking.hostName config.networking.domain;
-in {
-  networking = {
-    <link linkend="opt-networking.hostName">hostName</link> = "myhostname";
-    <link linkend="opt-networking.domain">domain</link> = "example.org";
-  };
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-
-  <link linkend="opt-services.postgresql.enable">services.postgresql.enable</link> = true;
-  <link linkend="opt-services.postgresql.initialScript">services.postgresql.initialScript</link> = pkgs.writeText "synapse-init.sql" ''
-    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
-    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
-      TEMPLATE template0
-      LC_COLLATE = "C"
-      LC_CTYPE = "C";
-  '';
-
-  services.nginx = {
-    <link linkend="opt-services.nginx.enable">enable</link> = true;
-    # only recommendedProxySettings and recommendedGzipSettings are strictly required,
-    # but the rest make sense as well
-    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
-    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
-
-    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-      # This host section can be placed on a different host than the rest,
-      # 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
-            # the client-server and server-server port for simplicity
-            server = { "m.server" = "${fqdn}:443"; };
-          in ''
-            add_header Content-Type application/json;
-            return 200 '${builtins.toJSON server}';
-          '';
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> =
-          let
-            client = {
-              "m.homeserver" =  { "base_url" = "https://${fqdn}"; };
-              "m.identity_server" =  { "base_url" = "https://vector.im"; };
-            };
-          # ACAO required to allow element-web on any URL to request this json file
-          in ''
-            add_header Content-Type application/json;
-            add_header Access-Control-Allow-Origin *;
-            return 200 '${builtins.toJSON client}';
-          '';
-      };
-
-      # Reverse proxy for Matrix client-server and server-server communication
-      ${fqdn} = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-
-        # Or do a redirect instead of the 404, or whatever is appropriate for you.
-        # But do not put a Matrix Web client here! See the Element web section below.
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = ''
-          return 404;
-        '';
-
-        # forward all Matrix API calls to the synapse Matrix homeserver
-        locations."/_matrix" = {
-          <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">proxyPass</link> = "http://[::1]:8008"; # without a trailing /
-        };
-      };
-    };
-  };
-  services.matrix-synapse = {
-    <link linkend="opt-services.matrix-synapse.enable">enable</link> = true;
-    <link linkend="opt-services.matrix-synapse.server_name">server_name</link> = config.networking.domain;
-    <link linkend="opt-services.matrix-synapse.listeners">listeners</link> = [
-      {
-        <link linkend="opt-services.matrix-synapse.listeners._.port">port</link> = 8008;
-        <link linkend="opt-services.matrix-synapse.listeners._.bind_address">bind_address</link> = "::1";
-        <link linkend="opt-services.matrix-synapse.listeners._.type">type</link> = "http";
-        <link linkend="opt-services.matrix-synapse.listeners._.tls">tls</link> = false;
-        <link linkend="opt-services.matrix-synapse.listeners._.x_forwarded">x_forwarded</link> = true;
-        <link linkend="opt-services.matrix-synapse.listeners._.resources">resources</link> = [
-          {
-            <link linkend="opt-services.matrix-synapse.listeners._.resources._.names">names</link> = [ "client" "federation" ];
-            <link linkend="opt-services.matrix-synapse.listeners._.resources._.compress">compress</link> = false;
-          }
-        ];
-      }
-    ];
-  };
-}
-</programlisting>
-  </para>
-
-  <para>
-   If the <code>A</code> and <code>AAAA</code> DNS records on
-   <literal>example.org</literal> do not point on the same host as the records
-   for <code>myhostname.example.org</code>, you can easily move the
-   <code>/.well-known</code> virtualHost section of the code to the host that
-   is serving <literal>example.org</literal>, while the rest stays on
-   <literal>myhostname.example.org</literal> with no other changes required.
-   This pattern also allows to seamlessly move the homeserver from
-   <literal>myhostname.example.org</literal> to
-   <literal>myotherhost.example.org</literal> by only changing the
-   <code>/.well-known</code> redirection target.
-  </para>
-
-  <para>
-   If you want to run a server with public registration by anybody, you can
-   then enable <literal><link linkend="opt-services.matrix-synapse.enable_registration">services.matrix-synapse.enable_registration</link> =
-   true;</literal>. Otherwise, or you can generate a registration secret with
-   <command>pwgen -s 64 1</command> and set it with
-   <option><link linkend="opt-services.matrix-synapse.registration_shared_secret">services.matrix-synapse.registration_shared_secret</link></option>. To
-   create a new user or admin, run the following after you have set the secret
-   and have rebuilt NixOS:
-<screen>
-<prompt>$ </prompt>nix run nixpkgs.matrix-synapse
-<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008
-<prompt>New user localpart: </prompt><replaceable>your-username</replaceable>
-<prompt>Password:</prompt>
-<prompt>Confirm password:</prompt>
-<prompt>Make admin [no]:</prompt>
-Success!
-</screen>
-   In the example, this would create a user with the Matrix Identifier
-   <literal>@your-username:example.org</literal>. Note that the registration
-   secret ends up in the nix store and therefore is world-readable by any user
-   on your machine, so it makes sense to only temporarily activate the
-   <link linkend="opt-services.matrix-synapse.registration_shared_secret">registration_shared_secret</link>
-   option until a better solution for NixOS is in place.
-  </para>
- </section>
- <section xml:id="module-services-matrix-element-web">
-  <title>Element (formerly known as Riot) Web Client</title>
-
-  <para>
-   <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is
-   the reference web client for Matrix and developed by the core team at
-   matrix.org. Element was formerly known as Riot.im, see the
-   <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link>
-   for more information. The following snippet can be optionally added to the code before
-   to complete the synapse installation with a web client served at
-   <code>https://element.myhostname.example.org</code> and
-   <code>https://element.example.org</code>. Alternatively, you can use the hosted
-   copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>,
-   or use other web clients or native client applications. Due to the
-   <literal>/.well-known</literal> urls set up done above, many clients should
-   fill in the required connection details automatically when you enter your
-   Matrix Identifier. See
-   <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
-   Matrix Now!</link> for a list of existing clients and their supported
-   featureset.
-<programlisting>
-{
-  services.nginx.virtualHosts."element.${fqdn}" = {
-    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
-      "element.${config.networking.domain}"
-    ];
-
-    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
-      conf = {
-        default_server_config."m.homeserver" = {
-          "base_url" = "https://${fqdn}";
-          "server_name" = "${fqdn}";
-        };
-      };
-    };
-  };
-}
-</programlisting>
-  </para>
-
-  <para>
-   Note that the Element developers do not recommend running Element and your Matrix
-   homeserver on the same fully-qualified domain name for security reasons. In
-   the example, this means that you should not reuse the
-   <literal>myhostname.example.org</literal> virtualHost to also serve Element,
-   but instead serve it on a different subdomain, like
-   <literal>element.example.org</literal> in the example. See the
-   <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Element
-   Important Security Notes</link> for more information on this subject.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
index d2b0f0da2ad92..7a149ff47e617 100644
--- a/nixos/modules/services/misc/mbpfan.nix
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.mbpfan;
   verbose = if cfg.verbose then "v" else "";
   settingsFormat = pkgs.formats.ini {};
-  settingsFile = settingsFormat.generate "config.conf" cfg.settings;
+  settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
 
 in {
   options.services.mbpfan = {
@@ -31,34 +31,34 @@ in {
 
     settings = mkOption {
       default = {};
-      description = "The INI configuration for Mbpfan.";
+      description = "INI configuration for Mbpfan.";
       type = types.submodule {
         freeformType = settingsFormat.type;
 
         options.general.min_fan1_speed = mkOption {
-          type = types.int;
+          type = types.nullOr types.int;
           default = 2000;
-          description = "The minimum fan speed.";
-        };
-        options.general.max_fan1_speed = mkOption {
-          type = types.int;
-          default = 6199;
-          description = "The maximum fan speed.";
+          description = ''
+            You can check minimum and maximum fan limits with
+            "cat /sys/devices/platform/applesmc.768/fan*_min" and
+            "cat /sys/devices/platform/applesmc.768/fan*_max" respectively.
+            Setting to null implies using default value from applesmc.
+          '';
         };
         options.general.low_temp = mkOption {
           type = types.int;
           default = 55;
-          description = "The low temperature.";
+          description = "If temperature is below this, fans will run at minimum speed.";
         };
         options.general.high_temp = mkOption {
           type = types.int;
           default = 58;
-          description = "The high temperature.";
+          description = "If temperature is above this, fan speed will gradually increase.";
         };
         options.general.max_temp = mkOption {
           type = types.int;
           default = 86;
-          description = "The maximum temperature.";
+          description = "If temperature is above this, fans will run at maximum speed.";
         };
         options.general.polling_interval = mkOption {
           type = types.int;
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 360cdbac2a1e9..ee5c0ef8d277b 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -366,6 +366,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
       serviceConfig.User = cfg.user;
+      serviceConfig.Group = cfg.group;
     };
 
     users.groups = optionalAttrs (cfg.group == "mediatomb") {
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
index ae57aaa6d4795..b75227effa04f 100644
--- a/nixos/modules/services/misc/moonraker.nix
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -79,6 +79,19 @@ in {
           for supported values.
         '';
       };
+
+      allowSystemControl = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow Moonraker to perform system-level operations.
+
+          Moonraker exposes APIs to perform system-level operations, such as
+          reboot, shutdown, and management of systemd units. See the
+          <link xlink:href="https://moonraker.readthedocs.io/en/latest/web_api/#machine-commands">documentation</link>
+          for details on what clients are able to do.
+        '';
+      };
     };
   };
 
@@ -86,6 +99,13 @@ in {
     warnings = optional (cfg.settings ? update_manager)
       ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'';
 
+    assertions = [
+      {
+        assertion = cfg.allowSystemControl -> config.security.polkit.enable;
+        message = "services.moonraker.allowSystemControl requires polkit to be enabled (security.polkit.enable).";
+      }
+    ];
+
     users.users = optionalAttrs (cfg.user == "moonraker") {
       moonraker = {
         group = cfg.group;
@@ -128,11 +148,31 @@ in {
         exec ${pkg}/bin/moonraker -c ${cfg.configDir}/moonraker-temp.cfg
       '';
 
+      # Needs `ip` command
+      path = [ pkgs.iproute2 ];
+
       serviceConfig = {
         WorkingDirectory = cfg.stateDir;
         Group = cfg.group;
         User = cfg.user;
       };
     };
+
+    security.polkit.extraConfig = lib.optionalString cfg.allowSystemControl ''
+      // nixos/moonraker: Allow Moonraker to perform system-level operations
+      //
+      // This was enabled via services.moonraker.allowSystemControl.
+      polkit.addRule(function(action, subject) {
+        if ((action.id == "org.freedesktop.systemd1.manage-units" ||
+             action.id == "org.freedesktop.login1.power-off" ||
+             action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
+             action.id == "org.freedesktop.login1.reboot" ||
+             action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
+             action.id.startsWith("org.freedesktop.packagekit.")) &&
+             subject.user == "${cfg.user}") {
+          return polkit.Result.YES;
+        }
+      });
+    '';
   };
 }
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
index 6a9eeb02095cc..5bf0e6bc00893 100644
--- a/nixos/modules/services/misc/nitter.nix
+++ b/nixos/modules/services/misc/nitter.nix
@@ -49,6 +49,13 @@ in
     services.nitter = {
       enable = mkEnableOption "If enabled, start Nitter.";
 
+      package = mkOption {
+        default = pkgs.nitter;
+        type = types.package;
+        defaultText = literalExpression "pkgs.nitter";
+        description = "The nitter derivation to use.";
+      };
+
       server = {
         address = mkOption {
           type =  types.str;
@@ -78,8 +85,8 @@ in
 
         staticDir = mkOption {
           type = types.path;
-          default = "${pkgs.nitter}/share/nitter/public";
-          defaultText = literalExpression ''"''${pkgs.nitter}/share/nitter/public"'';
+          default = "${cfg.package}/share/nitter/public";
+          defaultText = literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
           description = "Path to the static files directory.";
         };
 
@@ -270,7 +277,7 @@ in
           Add settings here to override NixOS module generated settings.
 
           Check the official repository for the available settings:
-          https://github.com/zedeus/nitter/blob/master/nitter.conf
+          https://github.com/zedeus/nitter/blob/master/nitter.example.conf
         '';
       };
 
@@ -306,8 +313,8 @@ in
           Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ];
           # Some parts of Nitter expect `public` folder in working directory,
           # see https://github.com/zedeus/nitter/issues/414
-          WorkingDirectory = "${pkgs.nitter}/share/nitter";
-          ExecStart = "${pkgs.nitter}/bin/nitter";
+          WorkingDirectory = "${cfg.package}/share/nitter";
+          ExecStart = "${cfg.package}/bin/nitter";
           ExecStartPre = "${preStart}";
           AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
           Restart = "on-failure";
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index ea57292fc4bf3..f6f74d12e1063 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -112,11 +112,11 @@ in
 
 {
   imports = [
-    (mkRenamedOptionModule [ "nix" "useChroot" ] [ "nix" "useSandbox" ])
-    (mkRenamedOptionModule [ "nix" "chrootDirs" ] [ "nix" "sandboxPaths" ])
-    (mkRenamedOptionModule [ "nix" "daemonIONiceLevel" ] [ "nix" "daemonIOSchedPriority" ])
+    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "useChroot" ]; to = [ "nix" "useSandbox" ]; })
+    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "chrootDirs" ]; to = [ "nix" "sandboxPaths" ]; })
+    (mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" "daemonIONiceLevel" ]; to = [ "nix" "daemonIOSchedPriority" ]; })
     (mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
-  ] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModule [ "nix" oldConf ] [ "nix" "settings" newConf ]) legacyConfMappings;
+  ] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" oldConf ]; to = [ "nix" "settings" newConf ]; }) legacyConfMappings;
 
   ###### interface
 
@@ -409,14 +409,14 @@ in
               to = mkOption {
                 type = referenceAttrs;
                 example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
-                description = "The flake reference <option>from></option> is rewritten to.";
+                description = "The flake reference <option>from</option> is rewritten to.";
               };
               flake = mkOption {
                 type = types.nullOr types.attrs;
                 default = null;
                 example = literalExpression "nixpkgs";
                 description = ''
-                  The flake input <option>from></option> is rewritten to.
+                  The flake input <option>from</option> is rewritten to.
                 '';
               };
               exact = mkOption {
@@ -680,7 +680,7 @@ in
               (if machine.sshKey != null then machine.sshKey else "-")
               (toString machine.maxJobs)
               (toString machine.speedFactor)
-              (concatStringsSep "," machine.supportedFeatures)
+              (concatStringsSep "," (machine.supportedFeatures ++ machine.mandatoryFeatures))
               (concatStringsSep "," machine.mandatoryFeatures)
             ]
             ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
@@ -708,6 +708,14 @@ in
 
     systemd.packages = [ nixPackage ];
 
+    # Will only work once https://github.com/NixOS/nix/pull/6285 is merged
+    # systemd.tmpfiles.packages = [ nixPackage ];
+
+    # Can be dropped for Nix > https://github.com/NixOS/nix/pull/6285
+    systemd.tmpfiles.rules = [
+      "d /nix/var/nix/daemon-socket 0755 root root - -"
+    ];
+
     systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
 
     systemd.services.nix-daemon =
@@ -726,10 +734,44 @@ in
             CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
             IOSchedulingClass = cfg.daemonIOSchedClass;
             IOSchedulingPriority = cfg.daemonIOSchedPriority;
-            LimitNOFILE = 4096;
+            LimitNOFILE = 1048576;
           };
 
         restartTriggers = [ nixConf ];
+
+        # `stopIfChanged = false` changes to switch behavior
+        # from   stop -> update units -> start
+        #   to   update units -> restart
+        #
+        # The `stopIfChanged` setting therefore controls a trade-off between a
+        # more predictable lifecycle, which runs the correct "version" of
+        # the `ExecStop` line, and on the other hand the availability of
+        # sockets during the switch, as the effectiveness of the stop operation
+        # depends on the socket being stopped as well.
+        #
+        # As `nix-daemon.service` does not make use of `ExecStop`, we prefer
+        # to keep the socket up and available. This is important for machines
+        # that run Nix-based services, such as automated build, test, and deploy
+        # services, that expect the daemon socket to be available at all times.
+        #
+        # Notably, the Nix client does not retry on failure to connect to the
+        # daemon socket, and the in-process RemoteStore instance will disable
+        # itself. This makes retries infeasible even for services that are
+        # aware of the issue. Failure to connect can affect not only new client
+        # processes, but also new RemoteStore instances in existing processes,
+        # as well as existing RemoteStore instances that have not saturated
+        # their connection pool.
+        #
+        # Also note that `stopIfChanged = true` does not kill existing
+        # connection handling daemons, as one might wish to happen before a
+        # breaking Nix upgrade (which is rare). The daemon forks that handle
+        # the individual connections split off into their own sessions, causing
+        # them not to be stopped by systemd.
+        # If a Nix upgrade does require all existing daemon processes to stop,
+        # nix-daemon must do so on its own accord, and only when the new version
+        # starts and detects that Nix's persistent state needs an upgrade.
+        stopIfChanged = false;
+
       };
 
     # Set up the environment variables for running Nix.
@@ -762,7 +804,7 @@ in
     nix.settings = mkMerge [
       {
         trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
-        substituters = [ "https://cache.nixos.org/" ];
+        substituters = mkAfter [ "https://cache.nixos.org/" ];
 
         system-features = mkDefault (
           [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
index a7a6a3b59644e..0fcb016010175 100644
--- a/nixos/modules/services/misc/nix-gc.nix
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -39,7 +39,7 @@ in
         type = types.str;
         example = "45min";
         description = ''
-          Add a randomized delay before each automatic upgrade.
+          Add a randomized delay before each garbage collection.
           The delay will be chosen between zero and this value.
           This value must be a time span in the format specified by
           <citerefentry><refentrytitle>systemd.time</refentrytitle>
@@ -81,8 +81,14 @@ in
   ###### implementation
 
   config = {
-
-    systemd.services.nix-gc = {
+    assertions = [
+      {
+        assertion = cfg.automatic -> config.nix.enable;
+        message = ''nix.gc.automatic requires nix.enable'';
+      }
+    ];
+
+    systemd.services.nix-gc = lib.mkIf config.nix.enable {
       description = "Nix Garbage Collector";
       script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
       startAt = optional cfg.automatic cfg.dates;
diff --git a/nixos/modules/services/misc/nix-optimise.nix b/nixos/modules/services/misc/nix-optimise.nix
index e02026d5f76c7..acf8177b146ae 100644
--- a/nixos/modules/services/misc/nix-optimise.nix
+++ b/nixos/modules/services/misc/nix-optimise.nix
@@ -37,8 +37,14 @@ in
   ###### implementation
 
   config = {
-
-    systemd.services.nix-optimise =
+    assertions = [
+      {
+        assertion = cfg.automatic -> config.nix.enable;
+        message = ''nix.optimise.automatic requires nix.enable'';
+      }
+    ];
+
+    systemd.services.nix-optimise = lib.mkIf config.nix.enable
       { description = "Nix Store Optimiser";
         # No point this if the nix daemon (and thus the nix store) is outside
         unitConfig.ConditionPathIsReadWrite = "/nix/var/nix/daemon-socket";
diff --git a/nixos/modules/services/misc/paperless-ng.nix b/nixos/modules/services/misc/paperless.nix
index db8082f072c3b..17cd555d7e1bd 100644
--- a/nixos/modules/services/misc/paperless-ng.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -2,22 +2,30 @@
 
 with lib;
 let
-  cfg = config.services.paperless-ng;
+  cfg = config.services.paperless;
 
   defaultUser = "paperless";
 
+  # Don't start a redis instance if the user sets a custom redis connection
+  enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
+  redisServer = config.services.redis.servers.paperless;
+
   env = {
     PAPERLESS_DATA_DIR = cfg.dataDir;
     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
-  } // lib.mapAttrs (_: toString) cfg.extraConfig;
+  } // (
+    lib.mapAttrs (_: toString) cfg.extraConfig
+  ) // (optionalAttrs enableRedis {
+    PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
+  });
 
   manage = let
     setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
   in pkgs.writeShellScript "manage" ''
     ${setupEnv}
-    exec ${cfg.package}/bin/paperless-ng "$@"
+    exec ${cfg.package}/bin/paperless-ngx "$@"
   '';
 
   # Secure the services
@@ -30,7 +38,7 @@ let
       "-/etc/hosts"
       "-/etc/localtime"
       "-/run/postgresql"
-    ];
+    ] ++ (optional enableRedis redisServer.unixSocket);
     BindPaths = [
       cfg.consumptionDir
       cfg.dataDir
@@ -44,11 +52,9 @@ let
     NoNewPrivileges = true;
     PrivateDevices = true;
     PrivateMounts = true;
-    # Needs to connect to redis
-    # PrivateNetwork = true;
+    PrivateNetwork = true;
     PrivateTmp = true;
     PrivateUsers = true;
-    ProcSubset = "pid";
     ProtectClock = true;
     # Breaks if the home dir of the user is in /home
     # Also does not add much value in combination with the TemporaryFileSystem.
@@ -61,10 +67,15 @@ let
     ProtectKernelModules = true;
     ProtectKernelTunables = true;
     ProtectProc = "invisible";
+    # Don't restrict ProcSubset because django-q requires read access to /proc/stat
+    # to query CPU and memory information.
+    # Note that /proc only contains processes of user `paperless`, so this is safe.
+    # ProcSubset = "pid";
     RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
     RestrictNamespaces = true;
     RestrictRealtime = true;
     RestrictSUIDSGID = true;
+    SupplementaryGroups = optional enableRedis redisServer.user;
     SystemCallArchitectures = "native";
     SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
     # Does not work well with the temporary root
@@ -72,29 +83,25 @@ let
   };
 in
 {
-  meta.maintainers = with maintainers; [ earvstedt Flakebi ];
+  meta.maintainers = with maintainers; [ erikarvstedt Flakebi ];
 
   imports = [
-    (mkRemovedOptionModule [ "services" "paperless"] ''
-      The paperless module has been removed as the upstream project died.
-      Users should migrate to the paperless-ng module (services.paperless-ng).
-      More information can be found in the NixOS 21.11 release notes.
-    '')
+    (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
   ];
 
-  options.services.paperless-ng = {
+  options.services.paperless = {
     enable = mkOption {
       type = lib.types.bool;
       default = false;
       description = ''
-        Enable Paperless-ng.
+        Enable Paperless.
 
         When started, the Paperless database is automatically created if it doesn't
         exist and updated if the Paperless package has changed.
         Both tasks are achieved by running a Django migration.
 
         A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
-        <literal>''${dataDir}/paperless-ng-manage</literal>.
+        <literal>''${dataDir}/paperless-manage</literal>.
       '';
     };
 
@@ -127,13 +134,13 @@ in
     passwordFile = mkOption {
       type = types.nullOr types.path;
       default = null;
-      example = "/run/keys/paperless-ng-password";
+      example = "/run/keys/paperless-password";
       description = ''
         A file containing the superuser password.
 
         A superuser is required to access the web interface.
         If unset, you can create a superuser manually by running
-        <literal>''${dataDir}/paperless-ng-manage createsuperuser</literal>.
+        <literal>''${dataDir}/paperless-manage createsuperuser</literal>.
 
         The default superuser name is <literal>admin</literal>. To change it, set
         option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
@@ -162,9 +169,9 @@ in
       type = types.attrs;
       default = {};
       description = ''
-        Extra paperless-ng config options.
+        Extra paperless config options.
 
-        See <link xlink:href="https://paperless-ng.readthedocs.io/en/latest/configuration.html">the documentation</link>
+        See <link xlink:href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html">the documentation</link>
         for available options.
       '';
       example = literalExpression ''
@@ -182,15 +189,14 @@ in
 
     package = mkOption {
       type = types.package;
-      default = pkgs.paperless-ng;
-      defaultText = literalExpression "pkgs.paperless-ng";
+      default = pkgs.paperless-ngx;
+      defaultText = literalExpression "pkgs.paperless-ngx";
       description = "The Paperless package to use.";
     };
   };
 
   config = mkIf cfg.enable {
-    # Enable redis if no special url is set
-    services.redis.enable = mkIf (!hasAttr "PAPERLESS_REDIS" env) true;
+    services.redis.servers.paperless.enable = mkIf enableRedis true;
 
     systemd.tmpfiles.rules = [
       "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
@@ -202,24 +208,28 @@ in
       )
     ];
 
-    systemd.services.paperless-ng-server = {
-      description = "Paperless document server";
+    systemd.services.paperless-scheduler = {
+      description = "Paperless scheduler";
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
-        ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
+        ExecStart = "${cfg.package}/bin/paperless-ngx qcluster";
         Restart = "on-failure";
+        # The `mbind` syscall is needed for running the classifier.
+        SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
+        # Needs to talk to mail server for automated import rules
+        PrivateNetwork = false;
       };
       environment = env;
       wantedBy = [ "multi-user.target" ];
-      wants = [ "paperless-ng-consumer.service" "paperless-ng-web.service" ];
+      wants = [ "paperless-consumer.service" "paperless-web.service" ];
 
       preStart = ''
-        ln -sf ${manage} ${cfg.dataDir}/paperless-ng-manage
+        ln -sf ${manage} ${cfg.dataDir}/paperless-manage
 
         # Auto-migrate on first run or if the package has changed
         versionFile="${cfg.dataDir}/src-version"
         if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
-          ${cfg.package}/bin/paperless-ng migrate
+          ${cfg.package}/bin/paperless-ngx migrate
           echo ${cfg.package} > "$versionFile"
         fi
       ''
@@ -230,18 +240,18 @@ in
         superuserStateFile="${cfg.dataDir}/superuser-state"
 
         if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
-          ${cfg.package}/bin/paperless-ng manage_superuser
+          ${cfg.package}/bin/paperless-ngx manage_superuser
           echo "$superuserState" > "$superuserStateFile"
         fi
       '';
+    } // optionalAttrs enableRedis {
+      after = [ "redis-paperless.service" ];
     };
 
-    # Password copying can't be implemented as a privileged preStart script
-    # in 'paperless-ng-server' because 'defaultServiceConfig' limits the filesystem
-    # paths accessible by the service.
-    systemd.services.paperless-ng-copy-password = mkIf (cfg.passwordFile != null) {
-      requiredBy = [ "paperless-ng-server.service" ];
-      before = [ "paperless-ng-server.service" ];
+    # Reading the user-provided password file requires root access
+    systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
+      requiredBy = [ "paperless-scheduler.service" ];
+      before = [ "paperless-scheduler.service" ];
       serviceConfig = {
         ExecStart = ''
           ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
@@ -251,27 +261,27 @@ in
       };
     };
 
-    systemd.services.paperless-ng-consumer = {
+    systemd.services.paperless-consumer = {
       description = "Paperless document consumer";
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
-        ExecStart = "${cfg.package}/bin/paperless-ng document_consumer";
+        ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
         Restart = "on-failure";
       };
       environment = env;
-      # Bind to `paperless-ng-server` so that the consumer never runs
+      # Bind to `paperless-scheduler` so that the consumer never runs
       # during migrations
-      bindsTo = [ "paperless-ng-server.service" ];
-      after = [ "paperless-ng-server.service" ];
+      bindsTo = [ "paperless-scheduler.service" ];
+      after = [ "paperless-scheduler.service" ];
     };
 
-    systemd.services.paperless-ng-web = {
+    systemd.services.paperless-web = {
       description = "Paperless web server";
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
         ExecStart = ''
           ${pkgs.python3Packages.gunicorn}/bin/gunicorn \
-            -c ${cfg.package}/lib/paperless-ng/gunicorn.conf.py paperless.asgi:application
+            -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
         '';
         Restart = "on-failure";
 
@@ -279,18 +289,20 @@ in
         CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
         # gunicorn needs setuid
         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
+        # Needs to serve web page
+        PrivateNetwork = false;
       };
       environment = env // {
         PATH = mkForce cfg.package.path;
-        PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
+        PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ngx/src";
       };
       # Allow the web interface to access the private /tmp directory of the server.
       # This is required to support uploading files via the web interface.
-      unitConfig.JoinsNamespaceOf = "paperless-ng-server.service";
-      # Bind to `paperless-ng-server` so that the web server never runs
+      unitConfig.JoinsNamespaceOf = "paperless-scheduler.service";
+      # Bind to `paperless-scheduler` so that the web server never runs
       # during migrations
-      bindsTo = [ "paperless-ng-server.service" ];
-      after = [ "paperless-ng-server.service" ];
+      bindsTo = [ "paperless-scheduler.service" ];
+      after = [ "paperless-scheduler.service" ];
     };
 
     users = optionalAttrs (cfg.user == defaultUser) {
diff --git a/nixos/modules/services/misc/persistent-evdev.nix b/nixos/modules/services/misc/persistent-evdev.nix
new file mode 100644
index 0000000000000..401d20010b12a
--- /dev/null
+++ b/nixos/modules/services/misc/persistent-evdev.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.persistent-evdev;
+  settingsFormat = pkgs.formats.json {};
+
+  configFile = settingsFormat.generate "persistent-evdev-config" {
+    cache = "/var/cache/persistent-evdev";
+    devices = lib.mapAttrs (virt: phys: "/dev/input/by-id/${phys}") cfg.devices;
+  };
+in
+{
+  options.services.persistent-evdev = {
+    enable = lib.mkEnableOption "virtual input devices that persist even if the backing device is hotplugged";
+
+    devices = lib.mkOption {
+      default = {};
+      type = with lib.types; attrsOf str;
+      description = ''
+        A set of virtual proxy device labels with backing physical device ids.
+
+        Physical devices should already exist in <filename class="devicefile">/dev/input/by-id/</filename>.
+        Proxy devices will be automatically given a <literal>uinput-</literal> prefix.
+
+        See the <link xlink:href="https://github.com/aiberia/persistent-evdev#example-usage-with-libvirt">
+        project page</link> for example configuration of virtual devices with libvirt
+        and remember to add <literal>uinput-*</literal> devices to the qemu
+        <literal>cgroup_device_acl</literal> list (see <xref linkend="opt-virtualisation.libvirtd.qemu.verbatimConfig"/>).
+      '';
+      example = lib.literalExpression ''
+        {
+          persist-mouse0 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-event-if01";
+          persist-mouse1 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-event-mouse";
+          persist-mouse2 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-if01-event-kbd";
+          persist-keyboard0 = "usb-Microsoft_Natural®_Ergonomic_Keyboard_4000-event-kbd";
+          persist-keyboard1 = "usb-Microsoft_Natural®_Ergonomic_Keyboard_4000-if01-event-kbd";
+        }
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services.persistent-evdev = {
+      documentation = [ "https://github.com/aiberia/persistent-evdev/blob/master/README.md" ];
+      description = "Persistent evdev proxy";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+        ExecStart = "${pkgs.persistent-evdev}/bin/persistent-evdev.py ${configFile}";
+        CacheDirectory = "persistent-evdev";
+      };
+    };
+
+    services.udev.packages = [ pkgs.persistent-evdev ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ lodi ];
+}
diff --git a/nixos/modules/services/misc/plex.nix b/nixos/modules/services/misc/plex.nix
index 7000d45975fc6..1cd8da768f48d 100644
--- a/nixos/modules/services/misc/plex.nix
+++ b/nixos/modules/services/misc/plex.nix
@@ -55,6 +55,19 @@ in
           symlinks in Plex's plugin directory will be cleared and this module
           will symlink all of the paths specified here to that directory.
         '';
+        example = literalExpression ''
+          [
+            (builtins.path {
+              name = "Audnexus.bundle";
+              path = pkgs.fetchFromGitHub {
+                owner = "djdembeck";
+                repo = "Audnexus.bundle";
+                rev = "v0.2.8";
+                sha256 = "sha256-IWOSz3vYL7zhdHan468xNc6C/eQ2C2BukQlaJNLXh7E=";
+              };
+            })
+          ]
+        '';
       };
 
       extraScanners = mkOption {
diff --git a/nixos/modules/services/misc/polaris.nix b/nixos/modules/services/misc/polaris.nix
new file mode 100644
index 0000000000000..68045af1528b5
--- /dev/null
+++ b/nixos/modules/services/misc/polaris.nix
@@ -0,0 +1,151 @@
+{ config
+, pkgs
+, lib
+, ...}:
+
+with lib;
+let
+  cfg = config.services.polaris;
+  settingsFormat = pkgs.formats.toml {};
+in
+{
+  options = {
+    services.polaris = {
+      enable = mkEnableOption "Polaris Music Server";
+
+      package = mkPackageOption pkgs "polaris" { };
+
+      user = mkOption {
+        type = types.str;
+        default = "polaris";
+        description = "User account under which Polaris runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "polaris";
+        description = "Group under which Polaris is run.";
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Polaris' auxiliary groups.";
+        example = literalExpression ''["media" "music"]'';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5050;
+        description = ''
+          The port which the Polaris REST api and web UI should listen to.
+          Note: polaris is hardcoded to listen to the hostname "0.0.0.0".
+        '';
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = {};
+        description = ''
+          Contents for the TOML Polaris config, applied each start.
+          Although poorly documented, an example may be found here:
+          <link xlink:href="https://github.com/agersant/polaris/blob/374d0ca56fc0a466d797a4b252e2078607476797/test-data/config.toml">test-config.toml</link>
+        '';
+        example = literalExpression ''
+          {
+            settings.reindex_every_n_seconds = 7*24*60*60; # weekly, default is 1800
+            settings.album_art_pattern =
+              "(cover|front|folder)\.(jpeg|jpg|png|bmp|gif)";
+            mount_dirs = [
+              {
+                name = "NAS";
+                source = "/mnt/nas/music";
+              }
+              {
+                name = "Local";
+                source = "/home/my_user/Music";
+              }
+            ];
+          }
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the configured port in the firewall.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.polaris = {
+      description = "Polaris Music Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        User = cfg.user;
+        Group = cfg.group;
+        DynamicUser = true;
+        SupplementaryGroups = cfg.extraGroups;
+        StateDirectory = "polaris";
+        CacheDirectory = "polaris";
+        ExecStart = escapeShellArgs ([
+          "${cfg.package}/bin/polaris"
+          "--foreground"
+          "--port" cfg.port
+          "--database" "/var/lib/${StateDirectory}/db.sqlite"
+          "--cache" "/var/cache/${CacheDirectory}"
+        ] ++ optionals (cfg.settings != {}) [
+          "--config" (settingsFormat.generate "polaris-config.toml" cfg.settings)
+        ]);
+        Restart = "on-failure";
+
+        # Security options:
+
+        #NoNewPrivileges = true; # implied by DynamicUser
+        #RemoveIPC = true; # implied by DynamicUser
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        #PrivateTmp = true; # implied by DynamicUser
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictRealtime = true;
+        #RestrictSUIDSGID = true; # implied by DynamicUser
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+        ];
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ pbsds ];
+}
diff --git a/nixos/modules/services/misc/radarr.nix b/nixos/modules/services/misc/radarr.nix
index 74444e24043f9..826d59da0af14 100644
--- a/nixos/modules/services/misc/radarr.nix
+++ b/nixos/modules/services/misc/radarr.nix
@@ -11,6 +11,14 @@ in
     services.radarr = {
       enable = mkEnableOption "Radarr";
 
+      package = mkOption {
+        description = "Radarr package to use";
+        default = pkgs.radarr;
+        defaultText = literalExpression "pkgs.radarr";
+        example = literalExpression "pkgs.radarr";
+        type = types.package;
+      };
+
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/radarr/.config/Radarr";
@@ -51,7 +59,7 @@ in
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
-        ExecStart = "${pkgs.radarr}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
+        ExecStart = "${cfg.package}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
deleted file mode 100644
index 685a132d35070..0000000000000
--- a/nixos/modules/services/misc/sourcehut/builds.nix
+++ /dev/null
@@ -1,236 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  scfg = cfg.builds;
-  rcfg = config.services.redis;
-  iniKey = "builds.sr.ht";
-
-  drv = pkgs.sourcehut.buildsrht;
-in
-{
-  options.services.sourcehut.builds = {
-    user = mkOption {
-      type = types.str;
-      default = "buildsrht";
-      description = ''
-        User for builds.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5002;
-      description = ''
-        Port on which the "builds" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "builds.sr.ht";
-      description = ''
-        PostgreSQL database name for builds.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/buildsrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/buildsrht"'';
-      description = ''
-        State path for builds.sr.ht.
-      '';
-    };
-
-    enableWorker = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Run workers for builds.sr.ht.
-      '';
-    };
-
-    images = mkOption {
-      type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
-      default = { };
-      example = lib.literalExpression ''(let
-          # Pinning unstable to allow usage with flakes and limit rebuilds.
-          pkgs_unstable = builtins.fetchGit {
-              url = "https://github.com/NixOS/nixpkgs";
-              rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
-              ref = "nixos-unstable";
-          };
-          image_from_nixpkgs = pkgs_unstable: (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
-            pkgs = (import pkgs_unstable {});
-          });
-        in
-        {
-          nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
-        }
-      )'';
-      description = ''
-        Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
-      '';
-    };
-
-  };
-
-  config = with scfg; let
-    image_dirs = lib.lists.flatten (
-      lib.attrsets.mapAttrsToList
-        (distro: revs:
-          lib.attrsets.mapAttrsToList
-            (rev: archs:
-              lib.attrsets.mapAttrsToList
-                (arch: image:
-                  pkgs.runCommand "buildsrht-images" { } ''
-                    mkdir -p $out/${distro}/${rev}/${arch}
-                    ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
-                  '')
-                archs)
-            revs)
-        scfg.images);
-    image_dir_pre = pkgs.symlinkJoin {
-      name = "builds.sr.ht-worker-images-pre";
-      paths = image_dirs ++ [
-        "${pkgs.sourcehut.buildsrht}/lib/images"
-      ];
-    };
-    image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
-      mkdir -p $out/images
-      cp -Lr ${image_dir_pre}/* $out/images
-    '';
-  in
-  lib.mkIf (cfg.enable && elem "builds" cfg.services) {
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
-          description = "builds.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0755 ${user} ${user} -"
-      ] ++ (lib.optionals cfg.builds.enableWorker
-        [ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
-      );
-
-      services = {
-        buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
-          {
-            after = [ "postgresql.service" "network.target" ];
-            requires = [ "postgresql.service" ];
-            wantedBy = [ "multi-user.target" ];
-
-            description = "builds.sr.ht website service";
-
-            serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-
-            # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
-          } // { preStart = " "; };
-
-        buildsrht-worker = {
-          enable = scfg.enableWorker;
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-          partOf = [ "buildsrht.service" ];
-          description = "builds.sr.ht worker service";
-          path = [ pkgs.openssh pkgs.docker ];
-          preStart = let qemuPackage = pkgs.qemu_kvm;
-          in ''
-            if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
-              # Create and import qemu:latest image for docker
-              ${
-                pkgs.dockerTools.streamLayeredImage {
-                  name = "qemu";
-                  tag = "latest";
-                  contents = [ qemuPackage ];
-                }
-              } | docker load
-              # Mark down current package version
-              printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
-            fi
-          '';
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Group = "nginx";
-            Restart = "always";
-          };
-          serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL builds.sr.ht is being served at (protocol://domain)
-      "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "builds.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "builds.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # builds.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "builds.sr.ht".oauth-client-id = mkDefault null;
-      "builds.sr.ht".oauth-client-secret = mkDefault null;
-      # The redis connection used for the celery worker
-      "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
-      # The shell used for ssh
-      "builds.sr.ht".shell = mkDefault "runner-shell";
-      # Register the builds.sr.ht dispatcher
-      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
-
-      # Location for build logs, images, and control command
-    } // lib.attrsets.optionalAttrs scfg.enableWorker {
-      # Default worker stores logs that are accessible via this address:port
-      "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
-      "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs";
-      "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
-      "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
-      "builds.sr.ht::worker".timeout = mkDefault "3m";
-    };
-
-    services.nginx.virtualHosts."logs.${cfg.originBase}" =
-      if scfg.enableWorker then {
-        listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name;
-        in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
-        locations."/logs".root = "${scfg.statePath}";
-      } else { };
-
-    services.nginx.virtualHosts."builds.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index 21551d7d5f033..3ff2837900eca 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -83,7 +83,7 @@ let
   python = pkgs.sourcehut.python.withPackages (ps: with ps; [
     gunicorn
     eventlet
-    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=1 flower
     flower
     # Sourcehut services
     srht
@@ -238,20 +238,32 @@ in
           };
           smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
           smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
-          smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
+          smtp-from = mkOption {
+            type = types.str;
+            description = "Outgoing SMTP FROM.";
+          };
           error-to = mkOptionNullOrStr "Address receiving application exceptions";
           error-from = mkOptionNullOrStr "Address sending application exceptions";
-          pgp-privkey = mkOptionNullOrStr ''
-            An absolute file path (which should be outside the Nix-store)
-            to an OpenPGP private key.
+          pgp-privkey = mkOption {
+            type = types.str;
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to an OpenPGP private key.
 
-            Your PGP key information (DO NOT mix up pub and priv here)
-            You must remove the password from your secret key, if present.
-            You can do this with <code>gpg --edit-key [key-id]</code>,
-            then use the <code>passwd</code> command and do not enter a new password.
-          '';
-          pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
-          pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
+              Your PGP key information (DO NOT mix up pub and priv here)
+              You must remove the password from your secret key, if present.
+              You can do this with <code>gpg --edit-key [key-id]</code>,
+              then use the <code>passwd</code> command and do not enter a new password.
+            '';
+          };
+          pgp-pubkey = mkOption {
+            type = with types; either path str;
+            description = "OpenPGP public key.";
+          };
+          pgp-key-id = mkOption {
+            type = types.str;
+            description = "OpenPGP key identifier.";
+          };
         };
         options.objects = {
           s3-upstream = mkOption {
@@ -905,6 +917,11 @@ in
       inherit configIniOfService;
       srvsrht = "buildsrht";
       port = 5002;
+      extraServices.buildsrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/buildsrht-api -b ${cfg.listenAddress}:${toString (cfg.builds.port + 100)}";
+      };
       # TODO: a celery worker on the master and worker are apparently needed
       extraServices.buildsrht-worker = let
         qemuPackage = pkgs.qemu_kvm;
@@ -928,13 +945,13 @@ in
           fi
         '';
         serviceConfig = {
-          ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+          ExecStart = "${pkgs.sourcehut.buildsrht}/bin/buildsrht-worker";
           BindPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
           LogsDirectory = [ "sourcehut/${serviceName}" ];
           RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
           StateDirectory = [ "sourcehut/${serviceName}" ];
           TimeoutStartSec = "1800s";
-          # builds.sr.ht-worker looks up ../config.ini
+          # buildsrht-worker looks up ../config.ini
           WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
         };
       };
@@ -952,12 +969,12 @@ in
           ) cfg.builds.images
         );
         image_dir_pre = pkgs.symlinkJoin {
-          name = "builds.sr.ht-worker-images-pre";
+          name = "buildsrht-worker-images-pre";
           paths = image_dirs;
             # FIXME: not working, apparently because ubuntu/latest is a broken link
             # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
         };
-        image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
+        image_dir = pkgs.runCommand "buildsrht-worker-images" { } ''
           mkdir -p $out/images
           cp -Lr ${image_dir_pre}/* $out/images
         '';
@@ -1018,7 +1035,7 @@ in
       inherit configIniOfService;
       mainService = mkMerge [ baseService {
         serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
-        preStart = mkIf (!versionAtLeast config.system.stateVersion "22.05") (mkBefore ''
+        preStart = mkIf (versionOlder config.system.stateVersion "22.05") (mkBefore ''
           # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
           (
           set +f
@@ -1081,6 +1098,11 @@ in
           };
         })
       ];
+      extraServices.gitsrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-api -b ${cfg.listenAddress}:${toString (cfg.git.port + 100)}";
+      };
       extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
         serviceConfig = {
           # Socket is passed by gitsrht-fcgiwrap.socket
@@ -1124,6 +1146,11 @@ in
         timerConfig.OnCalendar = ["daily"];
         timerConfig.AccuracySec = "1h";
       };
+      extraServices.hgsrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.hgsrht}/bin/hgsrht-api -b ${cfg.listenAddress}:${toString (cfg.hg.port + 100)}";
+      };
       extraConfig = mkMerge [
         {
           users.users.${cfg.hg.user}.shell = pkgs.bash;
@@ -1184,6 +1211,11 @@ in
       inherit configIniOfService;
       port = 5006;
       webhooks = true;
+      extraServices.listssrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.listssrht}/bin/listssrht-api -b ${cfg.listenAddress}:${toString (cfg.lists.port + 100)}";
+      };
       # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
       extraServices.listssrht-lmtp = {
         wants = [ "postfix.service" ];
@@ -1232,9 +1264,13 @@ in
       inherit configIniOfService;
       port = 5000;
       webhooks = true;
+      extraTimers.metasrht-daily.timerConfig = {
+        OnCalendar = ["daily"];
+        AccuracySec = "1h";
+      };
       extraServices.metasrht-api = {
         serviceConfig.Restart = "always";
-        serviceConfig.RestartSec = "2s";
+        serviceConfig.RestartSec = "5s";
         preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
           let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
               srv = head srvMatch;
@@ -1248,10 +1284,6 @@ in
           ) cfg.settings));
         serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
       };
-      extraTimers.metasrht-daily.timerConfig = {
-        OnCalendar = ["daily"];
-        AccuracySec = "1h";
-      };
       extraConfig = mkMerge [
         {
           assertions = [
@@ -1348,6 +1380,11 @@ in
       inherit configIniOfService;
       port = 5003;
       webhooks = true;
+      extraServices.todosrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.todosrht}/bin/todosrht-api -b ${cfg.listenAddress}:${toString (cfg.todo.port + 100)}";
+      };
       extraServices.todosrht-lmtp = {
         wants = [ "postfix.service" ];
         unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
diff --git a/nixos/modules/services/misc/sourcehut/dispatch.nix b/nixos/modules/services/misc/sourcehut/dispatch.nix
deleted file mode 100644
index 292a51d3e1c5d..0000000000000
--- a/nixos/modules/services/misc/sourcehut/dispatch.nix
+++ /dev/null
@@ -1,127 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.dispatch;
-  iniKey = "dispatch.sr.ht";
-
-  drv = pkgs.sourcehut.dispatchsrht;
-in
-{
-  options.services.sourcehut.dispatch = {
-    user = mkOption {
-      type = types.str;
-      default = "dispatchsrht";
-      description = ''
-        User for dispatch.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5005;
-      description = ''
-        Port on which the "dispatch" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "dispatch.sr.ht";
-      description = ''
-        PostgreSQL database name for dispatch.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/dispatchsrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/dispatchsrht"'';
-      description = ''
-        State path for dispatch.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) {
-
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          description = "dispatch.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-        after = [ "postgresql.service" "network.target" ];
-        requires = [ "postgresql.service" ];
-        wantedBy = [ "multi-user.target" ];
-
-        description = "dispatch.sr.ht website service";
-
-        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL dispatch.sr.ht is being served at (protocol://domain)
-      "dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "dispatch.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "dispatch.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "dispatch.sr.ht".oauth-client-id = mkDefault null;
-      "dispatch.sr.ht".oauth-client-secret = mkDefault null;
-
-      # Github Integration
-      "dispatch.sr.ht::github".oauth-client-id = mkDefault null;
-      "dispatch.sr.ht::github".oauth-client-secret = mkDefault null;
-
-      # Gitlab Integration
-      "dispatch.sr.ht::gitlab".enabled = mkDefault null;
-      "dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com";
-      "dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache";
-      # "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret";
-    };
-
-    services.nginx.virtualHosts."dispatch.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/git.nix b/nixos/modules/services/misc/sourcehut/git.nix
deleted file mode 100644
index 5ce16df8cd87c..0000000000000
--- a/nixos/modules/services/misc/sourcehut/git.nix
+++ /dev/null
@@ -1,217 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  scfg = cfg.git;
-  iniKey = "git.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.gitsrht;
-in
-{
-  options.services.sourcehut.git = {
-    user = mkOption {
-      type = types.str;
-      visible = false;
-      internal = true;
-      readOnly = true;
-      default = "git";
-      description = ''
-        User for git.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5001;
-      description = ''
-        Port on which the "git" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "git.sr.ht";
-      description = ''
-        PostgreSQL database name for git.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/gitsrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/gitsrht"'';
-      description = ''
-        State path for git.sr.ht.
-      '';
-    };
-
-    package = mkOption {
-      type = types.package;
-      default = pkgs.git;
-      defaultText = literalExpression "pkgs.git";
-      example = literalExpression "pkgs.gitFull";
-      description = ''
-        Git package for git.sr.ht. This can help silence collisions.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
-    # sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
-    environment.etc."ssh/gitsrht-dispatch" = {
-      mode = "0755";
-      text = ''
-        #! ${pkgs.stdenv.shell}
-        ${cfg.python}/bin/gitsrht-dispatch "$@"
-      '';
-    };
-
-    # Needs this in the $PATH when sshing into the server
-    environment.systemPackages = [ cfg.git.package ];
-
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
-          # Probably could use gitsrht-shell if output is restricted to just parameters...
-          shell = pkgs.bash;
-          description = "git.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services = {
-      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
-      fcgiwrap.enable = true;
-
-      openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"'';
-      openssh.authorizedKeysCommandUser = "root";
-      openssh.extraConfig = ''
-        PermitUserEnvironment SRHT_*
-      '';
-
-      postgresql = {
-        authentication = ''
-          local ${database} ${user} trust
-        '';
-        ensureDatabases = [ database ];
-        ensureUsers = [
-          {
-            name = user;
-            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-          }
-        ];
-      };
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        # /var/log is owned by root
-        "f /var/log/git-srht-shell 0644 ${user} ${user} -"
-
-        "d ${statePath} 0750 ${user} ${user} -"
-        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
-      ];
-
-      services = {
-        gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "redis.service" "postgresql.service" "network.target" ];
-          requires = [ "redis.service" "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          # Needs internally to create repos at the very least
-          path = [ pkgs.git ];
-          description = "git.sr.ht website service";
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-        };
-
-        gitsrht-webhooks = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "git.sr.ht webhooks service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-          };
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL git.sr.ht is being served at (protocol://domain)
-      "git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "git.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "git.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "git.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # The redis connection used for the webhooks worker
-      "git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
-
-      # A post-update script which is installed in every git repo.
-      "git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
-
-      # git.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "git.sr.ht".oauth-client-id = mkDefault null;
-      "git.sr.ht".oauth-client-secret = mkDefault null;
-      # Path to git repositories on disk
-      "git.sr.ht".repos = mkDefault "/var/lib/git";
-
-      "git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}";
-
-      # The authorized keys hook uses this to dispatch to various handlers
-      # The format is a program to exec into as the key, and the user to match as the
-      # value. When someone tries to log in as this user, this program is executed
-      # and is expected to omit an AuthorizedKeys file.
-      #
-      # Discard of the string context is in order to allow derivation-derived strings.
-      # This is safe if the relevant package is installed which will be the case if the setting is utilized.
-      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}";
-    };
-
-    services.nginx.virtualHosts."git.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht";
-      extraConfig = ''
-            location = /authorize {
-            proxy_pass http://${cfg.address}:${toString port};
-            proxy_pass_request_body off;
-            proxy_set_header Content-Length "";
-            proxy_set_header X-Original-URI $request_uri;
-        }
-            location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
-                auth_request /authorize;
-                root /var/lib/git;
-                fastcgi_pass unix:/run/fcgiwrap.sock;
-                fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend;
-                fastcgi_param PATH_INFO $uri;
-                fastcgi_param GIT_PROJECT_ROOT $document_root;
-                fastcgi_read_timeout 500s;
-                include ${pkgs.nginx}/conf/fastcgi_params;
-                gzip off;
-            }
-      '';
-
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/hg.nix b/nixos/modules/services/misc/sourcehut/hg.nix
deleted file mode 100644
index 6ba1df8b6ddb7..0000000000000
--- a/nixos/modules/services/misc/sourcehut/hg.nix
+++ /dev/null
@@ -1,175 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  scfg = cfg.hg;
-  iniKey = "hg.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.hgsrht;
-in
-{
-  options.services.sourcehut.hg = {
-    user = mkOption {
-      type = types.str;
-      internal = true;
-      readOnly = true;
-      default = "hg";
-      description = ''
-        User for hg.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5010;
-      description = ''
-        Port on which the "hg" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "hg.sr.ht";
-      description = ''
-        PostgreSQL database name for hg.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/hgsrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/hgsrht"'';
-      description = ''
-        State path for hg.sr.ht.
-      '';
-    };
-
-    cloneBundles = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) {
-    # In case it ever comes into being
-    environment.etc."ssh/hgsrht-dispatch" = {
-      mode = "0755";
-      text = ''
-        #! ${pkgs.stdenv.shell}
-        ${cfg.python}/bin/gitsrht-dispatch $@
-      '';
-    };
-
-    environment.systemPackages = [ pkgs.mercurial ];
-
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          # Assuming hg.sr.ht needs this too
-          shell = pkgs.bash;
-          description = "hg.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services = {
-      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ]
-        ++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles";
-
-      openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"'';
-      openssh.authorizedKeysCommandUser = "root";
-      openssh.extraConfig = ''
-        PermitUserEnvironment SRHT_*
-      '';
-
-      postgresql = {
-        authentication = ''
-          local ${database} ${user} trust
-        '';
-        ensureDatabases = [ database ];
-        ensureUsers = [
-          {
-            name = user;
-            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-          }
-        ];
-      };
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        # /var/log is owned by root
-        "f /var/log/hg-srht-shell 0644 ${user} ${user} -"
-
-        "d ${statePath} 0750 ${user} ${user} -"
-        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
-      ];
-
-      services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-        after = [ "redis.service" "postgresql.service" "network.target" ];
-        requires = [ "redis.service" "postgresql.service" ];
-        wantedBy = [ "multi-user.target" ];
-
-        path = [ pkgs.mercurial ];
-        description = "hg.sr.ht website service";
-
-        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL hg.sr.ht is being served at (protocol://domain)
-      "hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "hg.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "hg.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # The redis connection used for the webhooks worker
-      "hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
-      # A post-update script which is installed in every mercurial repo.
-      "hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup";
-      # hg.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "hg.sr.ht".oauth-client-id = mkDefault null;
-      "hg.sr.ht".oauth-client-secret = mkDefault null;
-      # Path to mercurial repositories on disk
-      "hg.sr.ht".repos = mkDefault "/var/lib/hg";
-      # Path to the srht mercurial extension
-      # (defaults to where the hgsrht code is)
-      # "hg.sr.ht".srhtext = mkDefault null;
-      # .hg/store size (in MB) past which the nightly job generates clone bundles.
-      # "hg.sr.ht".clone_bundle_threshold = mkDefault 50;
-      # Path to hg-ssh (if not in $PATH)
-      # "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh;
-
-      # The authorized keys hook uses this to dispatch to various handlers
-      # The format is a program to exec into as the key, and the user to match as the
-      # value. When someone tries to log in as this user, this program is executed
-      # and is expected to omit an AuthorizedKeys file.
-      #
-      # Uncomment the relevant lines to enable the various sr.ht dispatchers.
-      "hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}";
-    };
-
-    # TODO: requires testing and addition of hg-specific requirements
-    services.nginx.virtualHosts."hg.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/hub.nix b/nixos/modules/services/misc/sourcehut/hub.nix
deleted file mode 100644
index 7d137a765056c..0000000000000
--- a/nixos/modules/services/misc/sourcehut/hub.nix
+++ /dev/null
@@ -1,120 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.hub;
-  iniKey = "hub.sr.ht";
-
-  drv = pkgs.sourcehut.hubsrht;
-in
-{
-  options.services.sourcehut.hub = {
-    user = mkOption {
-      type = types.str;
-      default = "hubsrht";
-      description = ''
-        User for hub.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5014;
-      description = ''
-        Port on which the "hub" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "hub.sr.ht";
-      description = ''
-        PostgreSQL database name for hub.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/hubsrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/hubsrht"'';
-      description = ''
-        State path for hub.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) {
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          description = "hub.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-        after = [ "postgresql.service" "network.target" ];
-        requires = [ "postgresql.service" ];
-        wantedBy = [ "multi-user.target" ];
-
-        description = "hub.sr.ht website service";
-
-        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL hub.sr.ht is being served at (protocol://domain)
-      "hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "hub.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "hub.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "hub.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # hub.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "hub.sr.ht".oauth-client-id = mkDefault null;
-      "hub.sr.ht".oauth-client-secret = mkDefault null;
-    };
-
-    services.nginx.virtualHosts."${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht";
-    };
-    services.nginx.virtualHosts."hub.${cfg.originBase}" = {
-      globalRedirect = "${cfg.originBase}";
-      forceSSL = true;
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/lists.nix b/nixos/modules/services/misc/sourcehut/lists.nix
deleted file mode 100644
index 76f155caa05bc..0000000000000
--- a/nixos/modules/services/misc/sourcehut/lists.nix
+++ /dev/null
@@ -1,187 +0,0 @@
-# Email setup is fairly involved, useful references:
-# https://drewdevault.com/2018/08/05/Local-mail-server.html
-
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.lists;
-  iniKey = "lists.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.listssrht;
-in
-{
-  options.services.sourcehut.lists = {
-    user = mkOption {
-      type = types.str;
-      default = "listssrht";
-      description = ''
-        User for lists.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5006;
-      description = ''
-        Port on which the "lists" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "lists.sr.ht";
-      description = ''
-        PostgreSQL database name for lists.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/listssrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/listssrht"'';
-      description = ''
-        State path for lists.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) {
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          extraGroups = [ "postfix" ];
-          description = "lists.sr.ht user";
-        };
-      };
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services = {
-        listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "lists.sr.ht website service";
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-        };
-
-        listssrht-process = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "lists.sr.ht process service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info";
-          };
-        };
-
-        listssrht-lmtp = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "lists.sr.ht process service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/listssrht-lmtp";
-          };
-        };
-
-
-        listssrht-webhooks = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "lists.sr.ht webhooks service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
-          };
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL lists.sr.ht is being served at (protocol://domain)
-      "lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "lists.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "lists.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "lists.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # lists.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "lists.sr.ht".oauth-client-id = mkDefault null;
-      "lists.sr.ht".oauth-client-secret = mkDefault null;
-      # Outgoing email for notifications generated by users
-      "lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
-      # The redis connection used for the webhooks worker
-      "lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2";
-      # The redis connection used for the celery worker
-      "lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4";
-      # Network-key
-      "lists.sr.ht".network-key = mkDefault null;
-      # Allow creation
-      "lists.sr.ht".allow-new-lists = mkDefault "no";
-      # Posting Domain
-      "lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}";
-
-      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
-      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
-      "lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock";
-      # The lmtp daemon will make the unix socket group-read/write for users in this
-      # group.
-      "lists.sr.ht::worker".sock-group = mkDefault "postfix";
-      "lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md";
-      "lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html";
-
-    };
-
-    services.nginx.virtualHosts."lists.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/man.nix b/nixos/modules/services/misc/sourcehut/man.nix
deleted file mode 100644
index 8ca271c32ee3a..0000000000000
--- a/nixos/modules/services/misc/sourcehut/man.nix
+++ /dev/null
@@ -1,124 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.man;
-  iniKey = "man.sr.ht";
-
-  drv = pkgs.sourcehut.mansrht;
-in
-{
-  options.services.sourcehut.man = {
-    user = mkOption {
-      type = types.str;
-      default = "mansrht";
-      description = ''
-        User for man.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5004;
-      description = ''
-        Port on which the "man" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "man.sr.ht";
-      description = ''
-        PostgreSQL database name for man.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/mansrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/mansrht"'';
-      description = ''
-        State path for man.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) {
-    assertions =
-      [
-        {
-          assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni;
-          message = "man.sr.ht needs access to git.sr.ht.";
-        }
-      ];
-
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          description = "man.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-        after = [ "postgresql.service" "network.target" ];
-        requires = [ "postgresql.service" ];
-        wantedBy = [ "multi-user.target" ];
-
-        description = "man.sr.ht website service";
-
-        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL man.sr.ht is being served at (protocol://domain)
-      "man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "man.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "man.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "man.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # man.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "man.sr.ht".oauth-client-id = mkDefault null;
-      "man.sr.ht".oauth-client-secret = mkDefault null;
-    };
-
-    services.nginx.virtualHosts."man.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/meta.nix b/nixos/modules/services/misc/sourcehut/meta.nix
deleted file mode 100644
index 33e4f2332b53c..0000000000000
--- a/nixos/modules/services/misc/sourcehut/meta.nix
+++ /dev/null
@@ -1,213 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.meta;
-  iniKey = "meta.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.metasrht;
-in
-{
-  options.services.sourcehut.meta = {
-    user = mkOption {
-      type = types.str;
-      default = "metasrht";
-      description = ''
-        User for meta.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5000;
-      description = ''
-        Port on which the "meta" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "meta.sr.ht";
-      description = ''
-        PostgreSQL database name for meta.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/metasrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/metasrht"'';
-      description = ''
-        State path for meta.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) {
-    assertions =
-      [
-        {
-          assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null);
-          message = "If meta.sr.ht::billing is enabled, the keys should be defined.";
-        }
-      ];
-
-    users = {
-      users = {
-        ${user} = {
-          isSystemUser = true;
-          group = user;
-          description = "meta.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ];
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services = {
-        metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "meta.sr.ht website service";
-
-          preStart = ''
-            # Configure client(s) as "preauthorized"
-            ${concatMapStringsSep "\n\n"
-              (attr: ''
-                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
-                  # Configure ${attr}'s OAuth client as "preauthorized"
-                  psql ${database} \
-                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
-
-                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
-                fi
-              '')
-              (builtins.attrNames (filterAttrs
-                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
-                cfg.settings))}
-          '';
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-        };
-
-        metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "meta.sr.ht api service";
-
-          preStart = ''
-            # Configure client(s) as "preauthorized"
-            ${concatMapStringsSep "\n\n"
-              (attr: ''
-                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
-                  # Configure ${attr}'s OAuth client as "preauthorized"
-                  psql ${database} \
-                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
-
-                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
-                fi
-              '')
-              (builtins.attrNames (filterAttrs
-                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
-                cfg.settings))}
-          '';
-
-          serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}";
-        };
-
-        metasrht-webhooks = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "meta.sr.ht webhooks service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
-          };
-
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL meta.sr.ht is being served at (protocol://domain)
-      "meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "meta.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "meta.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "meta.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # If "yes", the user will be sent the stock sourcehut welcome emails after
-      # signup (requires cron to be configured properly). These are specific to the
-      # sr.ht instance so you probably want to patch these before enabling this.
-      "meta.sr.ht".welcome-emails = mkDefault "no";
-
-      # The redis connection used for the webhooks worker
-      "meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6";
-
-      # If "no", public registration will not be permitted.
-      "meta.sr.ht::settings".registration = mkDefault "no";
-      # Where to redirect new users upon registration
-      "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}";
-      # How many invites each user is issued upon registration (only applicable if
-      # open registration is disabled)
-      "meta.sr.ht::settings".user-invites = mkDefault 5;
-
-      # Origin URL for API, 100 more than web
-      "meta.sr.ht".api-origin = mkDefault "http://localhost:5100";
-
-      # You can add aliases for the client IDs of commonly used OAuth clients here.
-      #
-      # Example:
-      "meta.sr.ht::aliases" = mkDefault { };
-      # "meta.sr.ht::aliases"."git.sr.ht" = 12345;
-
-      # "yes" to enable the billing system
-      "meta.sr.ht::billing".enabled = mkDefault "no";
-      # Get your keys at https://dashboard.stripe.com/account/apikeys
-      "meta.sr.ht::billing".stripe-public-key = mkDefault null;
-      "meta.sr.ht::billing".stripe-secret-key = mkDefault null;
-    };
-
-    services.nginx.virtualHosts."meta.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/paste.nix b/nixos/modules/services/misc/sourcehut/paste.nix
deleted file mode 100644
index b481ebaf89178..0000000000000
--- a/nixos/modules/services/misc/sourcehut/paste.nix
+++ /dev/null
@@ -1,135 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.paste;
-  iniKey = "paste.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.pastesrht;
-in
-{
-  options.services.sourcehut.paste = {
-    user = mkOption {
-      type = types.str;
-      default = "pastesrht";
-      description = ''
-        User for paste.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5011;
-      description = ''
-        Port on which the "paste" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "paste.sr.ht";
-      description = ''
-        PostgreSQL database name for paste.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/pastesrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/pastesrht"'';
-      description = ''
-        State path for pastesrht.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) {
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          description = "paste.sr.ht user";
-        };
-      };
-
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services = {
-        pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "paste.sr.ht website service";
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-        };
-
-        pastesrht-webhooks = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "paste.sr.ht webhooks service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
-          };
-
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL paste.sr.ht is being served at (protocol://domain)
-      "paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "paste.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "paste.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "paste.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # paste.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "paste.sr.ht".oauth-client-id = mkDefault null;
-      "paste.sr.ht".oauth-client-secret = mkDefault null;
-      "paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5";
-    };
-
-    services.nginx.virtualHosts."paste.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
index f1706ad0a6a8a..4ecc7a7266907 100644
--- a/nixos/modules/services/misc/sourcehut/service.nix
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -148,7 +148,7 @@ in
     redis = {
       host = mkOption {
         type = types.str;
-        default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
+        default = "unix:///run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
         example = "redis://shared.wireguard:6379/0";
         description = ''
           The redis host URL. This is used for caching and temporary storage, and must
diff --git a/nixos/modules/services/misc/sourcehut/todo.nix b/nixos/modules/services/misc/sourcehut/todo.nix
deleted file mode 100644
index 262fa48f59d4d..0000000000000
--- a/nixos/modules/services/misc/sourcehut/todo.nix
+++ /dev/null
@@ -1,163 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-let
-  cfg = config.services.sourcehut;
-  opt = options.services.sourcehut;
-  cfgIni = cfg.settings;
-  scfg = cfg.todo;
-  iniKey = "todo.sr.ht";
-
-  rcfg = config.services.redis;
-  drv = pkgs.sourcehut.todosrht;
-in
-{
-  options.services.sourcehut.todo = {
-    user = mkOption {
-      type = types.str;
-      default = "todosrht";
-      description = ''
-        User for todo.sr.ht.
-      '';
-    };
-
-    port = mkOption {
-      type = types.port;
-      default = 5003;
-      description = ''
-        Port on which the "todo" module should listen.
-      '';
-    };
-
-    database = mkOption {
-      type = types.str;
-      default = "todo.sr.ht";
-      description = ''
-        PostgreSQL database name for todo.sr.ht.
-      '';
-    };
-
-    statePath = mkOption {
-      type = types.path;
-      default = "${cfg.statePath}/todosrht";
-      defaultText = literalExpression ''"''${config.${opt.statePath}}/todosrht"'';
-      description = ''
-        State path for todo.sr.ht.
-      '';
-    };
-  };
-
-  config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) {
-    users = {
-      users = {
-        "${user}" = {
-          isSystemUser = true;
-          group = user;
-          extraGroups = [ "postfix" ];
-          description = "todo.sr.ht user";
-        };
-      };
-      groups = {
-        "${user}" = { };
-      };
-    };
-
-    services.postgresql = {
-      authentication = ''
-        local ${database} ${user} trust
-      '';
-      ensureDatabases = [ database ];
-      ensureUsers = [
-        {
-          name = user;
-          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
-        }
-      ];
-    };
-
-    systemd = {
-      tmpfiles.rules = [
-        "d ${statePath} 0750 ${user} ${user} -"
-      ];
-
-      services = {
-        todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "todo.sr.ht website service";
-
-          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
-        };
-
-       todosrht-lmtp = {
-         after = [ "postgresql.service" "network.target" ];
-         bindsTo = [ "postgresql.service" ];
-         wantedBy = [ "multi-user.target" ];
-
-         description = "todo.sr.ht process service";
-         serviceConfig = {
-           Type = "simple";
-           User = user;
-           Restart = "always";
-           ExecStart = "${cfg.python}/bin/todosrht-lmtp";
-         };
-       };
-
-        todosrht-webhooks = {
-          after = [ "postgresql.service" "network.target" ];
-          requires = [ "postgresql.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          description = "todo.sr.ht webhooks service";
-          serviceConfig = {
-            Type = "simple";
-            User = user;
-            Restart = "always";
-            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
-          };
-
-        };
-      };
-    };
-
-    services.sourcehut.settings = {
-      # URL todo.sr.ht is being served at (protocol://domain)
-      "todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}";
-      # Address and port to bind the debug server to
-      "todo.sr.ht".debug-host = mkDefault "0.0.0.0";
-      "todo.sr.ht".debug-port = mkDefault port;
-      # Configures the SQLAlchemy connection string for the database.
-      "todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
-      # Set to "yes" to automatically run migrations on package upgrade.
-      "todo.sr.ht".migrate-on-upgrade = mkDefault "yes";
-      # todo.sr.ht's OAuth client ID and secret for meta.sr.ht
-      # Register your client at meta.example.org/oauth
-      "todo.sr.ht".oauth-client-id = mkDefault null;
-      "todo.sr.ht".oauth-client-secret = mkDefault null;
-      # Outgoing email for notifications generated by users
-      "todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
-      # The redis connection used for the webhooks worker
-      "todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
-      # Network-key
-      "todo.sr.ht".network-key = mkDefault null;
-
-      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
-      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
-      "todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock";
-      # The lmtp daemon will make the unix socket group-read/write for users in this
-      # group.
-      "todo.sr.ht::mail".sock-group = mkDefault "postfix";
-
-      "todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}";
-    };
-
-    services.nginx.virtualHosts."todo.${cfg.originBase}" = {
-      forceSSL = true;
-      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
-      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
-      locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht";
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
index a894caed1a34a..e20804929981f 100644
--- a/nixos/modules/services/misc/taskserver/default.nix
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -106,7 +106,7 @@ let
 
   certtool = "${pkgs.gnutls.bin}/bin/certtool";
 
-  nixos-taskserver = pkgs.pythonPackages.buildPythonApplication {
+  nixos-taskserver = with pkgs.python3.pkgs; buildPythonApplication {
     name = "nixos-taskserver";
 
     src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } ''
@@ -129,7 +129,7 @@ let
       EOF
     '';
 
-    propagatedBuildInputs = [ pkgs.pythonPackages.click ];
+    propagatedBuildInputs = [ click ];
   };
 
 in {
@@ -138,12 +138,13 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = ''
+        description = let
+          url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver";
+        in ''
           Whether to enable the Taskwarrior server.
 
           More instructions about NixOS in conjuction with Taskserver can be
-          found in the NixOS manual at
-          <olink targetdoc="manual" targetptr="module-taskserver"/>.
+          found <link xlink:href="${url}">in the NixOS manual</link>.
         '';
       };
 
@@ -276,10 +277,6 @@ in {
         example = "::";
         description = ''
           The address (IPv4, IPv6 or DNS) to listen on.
-
-          If the value is something else than <literal>localhost</literal> the
-          port defined by <option>listenPort</option> is automatically added to
-          <option>networking.firewall.allowedTCPPorts</option>.
         '';
       };
 
@@ -291,6 +288,14 @@ in {
         '';
       };
 
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the firewall for the specified Taskserver port.
+        '';
+      };
+
       fqdn = mkOption {
         type = types.str;
         default = "localhost";
@@ -559,7 +564,7 @@ in {
         '';
       };
     })
-    (mkIf (cfg.enable && cfg.listenHost != "localhost") {
+    (mkIf (cfg.enable && cfg.openFirewall) {
       networking.firewall.allowedTCPPorts = [ cfg.listenPort ];
     })
   ];
diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml
index 5656bb85b373b..f6ead7c37857a 100644
--- a/nixos/modules/services/misc/taskserver/doc.xml
+++ b/nixos/modules/services/misc/taskserver/doc.xml
@@ -1,7 +1,7 @@
 <chapter xmlns="http://docbook.org/ns/docbook"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     version="5.0"
-    xml:id="module-taskserver">
+    xml:id="module-services-taskserver">
  <title>Taskserver</title>
  <para>
   Taskserver is the server component of
diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py
index 22a3d8d5311be..fec05728b2b6b 100644
--- a/nixos/modules/services/misc/taskserver/helper-tool.py
+++ b/nixos/modules/services/misc/taskserver/helper-tool.py
@@ -90,7 +90,7 @@ def certtool_cmd(*args, **kwargs):
     """
     return subprocess.check_output(
         [CERTTOOL_COMMAND] + list(args),
-        preexec_fn=lambda: os.umask(0077),
+        preexec_fn=lambda: os.umask(0o077),
         stderr=subprocess.STDOUT,
         **kwargs
     )
@@ -164,7 +164,7 @@ def generate_key(org, user):
     pubcert = os.path.join(basedir, "public.cert")
 
     try:
-        os.makedirs(basedir, mode=0700)
+        os.makedirs(basedir, mode=0o700)
 
         certtool_cmd("-p", "--bits", CERT_BITS, "--outfile", privkey)
 
@@ -301,7 +301,7 @@ class Organisation(object):
             return None
         if name not in self.users.keys():
             output = taskd_cmd("add", "user", self.name, name,
-                               capture_stdout=True)
+                               capture_stdout=True, encoding='utf-8')
             key = RE_USERKEY.search(output)
             if key is None:
                 msg = "Unable to find key while creating user {}."
@@ -412,9 +412,9 @@ class Manager(object):
         if org is not None:
             if self.ignore_imperative and is_imperative(name):
                 return
-            for user in org.users.keys():
+            for user in list(org.users.keys()):
                 org.del_user(user)
-            for group in org.groups.keys():
+            for group in list(org.groups.keys()):
                 org.del_group(group)
             taskd_cmd("remove", "org", name)
             del self._lazy_orgs[name]
diff --git a/nixos/modules/services/misc/uhub.nix b/nixos/modules/services/misc/uhub.nix
index 0d0a8c2a4cb81..99774fbb920a0 100644
--- a/nixos/modules/services/misc/uhub.nix
+++ b/nixos/modules/services/misc/uhub.nix
@@ -80,11 +80,12 @@ in {
           tls_enable = cfg.enableTLS;
           file_plugins = pkgs.writeText "uhub-plugins.conf"
             (lib.strings.concatStringsSep "\n" (map ({ plugin, settings }:
-              "plugin ${plugin} ${
-                toString
-                (lib.attrsets.mapAttrsToList (key: value: ''"${key}=${value}"'')
-                  settings)
-              }") cfg.plugins));
+              ''
+                plugin ${plugin} "${
+                  toString
+                  (lib.attrsets.mapAttrsToList (key: value: "${key}=${value}")
+                    settings)
+                }"'') cfg.plugins));
         };
       in {
         name = "uhub/${name}.conf";
@@ -104,6 +105,9 @@ in {
           ExecStart = "${pkg}/bin/uhub -c /etc/uhub/${name}.conf -L";
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
           DynamicUser = true;
+
+          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+          CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
         };
       };
     }) hubs;
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 407742f72ad5a..a557e742b7cfa 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -254,7 +254,7 @@ in {
                 location /cgi-bin {
                   gzip off;
 
-                  include ${pkgs.nginx}/conf/fastcgi_params;
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
                   fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
                   fastcgi_param HTTP_PROXY "";
                   fastcgi_intercept_errors on;
@@ -270,7 +270,7 @@ in {
                   try_files $uri =404;
                   fastcgi_index index.php;
 
-                  include ${pkgs.nginx}/conf/fastcgi_params;
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
                   fastcgi_param SCRIPT_FILENAME $request_filename;
                   fastcgi_param HTTP_PROXY "";
 
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index 3809a93a61e18..fefbf9a86de49 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -114,6 +114,13 @@ in {
       type = types.package;
     };
 
+    jre = mkOption {
+      description = "The JRE with which to run Zookeeper";
+      default = cfg.package.jre;
+      defaultText = literalExpression "pkgs.zookeeper.jre";
+      example = literalExpression "pkgs.jre";
+      type = types.package;
+    };
   };
 
 
@@ -131,7 +138,7 @@ in {
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java \
+          ${cfg.jre}/bin/java \
             -cp "${cfg.package}/lib/*:${configDir}" \
             ${escapeShellArgs cfg.extraCmdLineOptions} \
             -Dzookeeper.datadir.autocreate=false \
diff --git a/nixos/modules/services/monitoring/collectd.nix b/nixos/modules/services/monitoring/collectd.nix
index 8d81737a3ef0d..1b9af58575604 100644
--- a/nixos/modules/services/monitoring/collectd.nix
+++ b/nixos/modules/services/monitoring/collectd.nix
@@ -5,36 +5,15 @@ with lib;
 let
   cfg = config.services.collectd;
 
-  unvalidated_conf = pkgs.writeText "collectd-unvalidated.conf" ''
-    BaseDir "${cfg.dataDir}"
-    AutoLoadPlugin ${boolToString cfg.autoLoadPlugin}
-    Hostname "${config.networking.hostName}"
-
-    LoadPlugin syslog
-    <Plugin "syslog">
-      LogLevel "info"
-      NotifyLevel "OKAY"
-    </Plugin>
-
-    ${concatStrings (mapAttrsToList (plugin: pluginConfig: ''
-      LoadPlugin ${plugin}
-      <Plugin "${plugin}">
-      ${pluginConfig}
-      </Plugin>
-    '') cfg.plugins)}
-
-    ${concatMapStrings (f: ''
-      Include "${f}"
-    '') cfg.include}
-
-    ${cfg.extraConfig}
-  '';
+  baseDirLine = ''BaseDir "${cfg.dataDir}"'';
+  unvalidated_conf = pkgs.writeText "collectd-unvalidated.conf" cfg.extraConfig;
 
   conf = if cfg.validateConfig then
     pkgs.runCommand "collectd.conf" {} ''
       echo testing ${unvalidated_conf}
+      cp ${unvalidated_conf} collectd.conf
       # collectd -t fails if BaseDir does not exist.
-      sed '1s/^BaseDir.*$/BaseDir "."/' ${unvalidated_conf} > collectd.conf
+      substituteInPlace collectd.conf --replace ${lib.escapeShellArgs [ baseDirLine ]} 'BaseDir "."'
       ${package}/bin/collectd -t -C collectd.conf
       cp ${unvalidated_conf} $out
     '' else unvalidated_conf;
@@ -123,7 +102,8 @@ in {
     extraConfig = mkOption {
       default = "";
       description = ''
-        Extra configuration for collectd.
+        Extra configuration for collectd. Use mkBefore to add lines before the
+        default config, and mkAfter to add them below.
       '';
       type = lines;
     };
@@ -131,6 +111,30 @@ in {
   };
 
   config = mkIf cfg.enable {
+    # 1200 is after the default (1000) but before mkAfter (1500).
+    services.collectd.extraConfig = lib.mkOrder 1200 ''
+      ${baseDirLine}
+      AutoLoadPlugin ${boolToString cfg.autoLoadPlugin}
+      Hostname "${config.networking.hostName}"
+
+      LoadPlugin syslog
+      <Plugin "syslog">
+        LogLevel "info"
+        NotifyLevel "OKAY"
+      </Plugin>
+
+      ${concatStrings (mapAttrsToList (plugin: pluginConfig: ''
+        LoadPlugin ${plugin}
+        <Plugin "${plugin}">
+        ${pluginConfig}
+        </Plugin>
+      '') cfg.plugins)}
+
+      ${concatMapStrings (f: ''
+        Include "${f}"
+      '') cfg.include}
+    '';
+
     systemd.tmpfiles.rules = [
       "d '${cfg.dataDir}' - ${cfg.user} - - -"
     ];
diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix
new file mode 100644
index 0000000000000..bbeda18464707
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana-agent.nix
@@ -0,0 +1,143 @@
+{ lib, pkgs, config, generators, ... }:
+with lib;
+let
+  cfg = config.services.grafana-agent;
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "grafana-agent.yaml" cfg.settings;
+in
+{
+  meta = {
+    maintainers = with maintainers; [ flokli zimbatm ];
+  };
+
+  options.services.grafana-agent = {
+    enable = mkEnableOption "grafana-agent";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.grafana-agent;
+      defaultText = "pkgs.grafana-agent";
+      description = "The grafana-agent package to use.";
+    };
+
+    credentials = mkOption {
+      description = ''
+        Credentials to load at service startup. Keys that are UPPER_SNAKE will be loaded as env vars. Values are absolute paths to the credentials.
+      '';
+      type = types.attrsOf types.str;
+      default = { };
+
+      example = {
+        logs_remote_write_password = "/run/keys/grafana_agent_logs_remote_write_password";
+        LOGS_REMOTE_WRITE_URL = "/run/keys/grafana_agent_logs_remote_write_url";
+        LOGS_REMOTE_WRITE_USERNAME = "/run/keys/grafana_agent_logs_remote_write_username";
+        metrics_remote_write_password = "/run/keys/grafana_agent_metrics_remote_write_password";
+        METRICS_REMOTE_WRITE_URL = "/run/keys/grafana_agent_metrics_remote_write_url";
+        METRICS_REMOTE_WRITE_USERNAME = "/run/keys/grafana_agent_metrics_remote_write_username";
+      };
+    };
+
+    settings = mkOption {
+      description = ''
+        Configuration for <package>grafana-agent</package>.
+
+        See https://grafana.com/docs/agent/latest/configuration/
+      '';
+
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+      };
+
+      default = {
+        metrics = {
+          wal_directory = "\${STATE_DIRECTORY}";
+          global.scrape_interval = "5s";
+        };
+        integrations = {
+          agent.enabled = true;
+          agent.scrape_integration = true;
+          node_exporter.enabled = true;
+          replace_instance_label = true;
+        };
+      };
+
+      example = {
+        metrics.global.remote_write = [{
+          url = "\${METRICS_REMOTE_WRITE_URL}";
+          basic_auth.username = "\${METRICS_REMOTE_WRITE_USERNAME}";
+          basic_auth.password_file = "\${CREDENTIALS_DIRECTORY}/metrics_remote_write_password";
+        }];
+        logs.configs = [{
+          name = "default";
+          scrape_configs = [
+            {
+              job_name = "journal";
+              journal = {
+                max_age = "12h";
+                labels.job = "systemd-journal";
+              };
+              relabel_configs = [
+                {
+                  source_labels = [ "__journal__systemd_unit" ];
+                  target_label = "systemd_unit";
+                }
+                {
+                  source_labels = [ "__journal__hostname" ];
+                  target_label = "nodename";
+                }
+                {
+                  source_labels = [ "__journal_syslog_identifier" ];
+                  target_label = "syslog_identifier";
+                }
+              ];
+            }
+          ];
+          positions.filename = "\${STATE_DIRECTORY}/loki_positions.yaml";
+          clients = [{
+            url = "\${LOGS_REMOTE_WRITE_URL}";
+            basic_auth.username = "\${LOGS_REMOTE_WRITE_USERNAME}";
+            basic_auth.password_file = "\${CREDENTIALS_DIRECTORY}/logs_remote_write_password";
+          }];
+        }];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.grafana-agent = {
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        set -euo pipefail
+        shopt -u nullglob
+
+        # Load all credentials into env if they are in UPPER_SNAKE form.
+        if [[ -n "''${CREDENTIALS_DIRECTORY:-}" ]]; then
+          for file in "$CREDENTIALS_DIRECTORY"/*; do
+            key=$(basename "$file")
+            if [[ $key =~ ^[A-Z0-9_]+$ ]]; then
+              echo "Environ $key"
+              export "$key=$(< "$file")"
+            fi
+          done
+        fi
+
+        # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part.
+        export HOSTNAME=$(< /proc/sys/kernel/hostname)
+
+        exec ${cfg.package}/bin/agent -config.expand-env -config.file ${configFile}
+      '';
+      serviceConfig = {
+        Restart = "always";
+        DynamicUser = true;
+        RestartSec = 2;
+        SupplementaryGroups = [
+          # allow to read the systemd journal for loki log forwarding
+          "systemd-journal"
+        ];
+        StateDirectory = "grafana-agent";
+        LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
+        Type = "simple";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 81fca33f5fec4..68b4796f4f4ec 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -14,6 +14,7 @@ let
     PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
     PATHS_LOGS = "${cfg.dataDir}/log";
 
+    SERVER_SERVE_FROM_SUBPATH = boolToString cfg.server.serveFromSubPath;
     SERVER_PROTOCOL = cfg.protocol;
     SERVER_HTTP_ADDR = cfg.addr;
     SERVER_HTTP_PORT = cfg.port;
@@ -41,9 +42,23 @@ let
     USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg;
     USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole;
 
+    AUTH_DISABLE_LOGIN_FORM = boolToString cfg.auth.disableLoginForm;
+
     AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
     AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
     AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
+
+    AUTH_AZUREAD_NAME = "Azure AD";
+    AUTH_AZUREAD_ENABLED = boolToString cfg.auth.azuread.enable;
+    AUTH_AZUREAD_ALLOW_SIGN_UP = boolToString cfg.auth.azuread.allowSignUp;
+    AUTH_AZUREAD_CLIENT_ID = cfg.auth.azuread.clientId;
+    AUTH_AZUREAD_SCOPES = "openid email profile";
+    AUTH_AZUREAD_AUTH_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/authorize";
+    AUTH_AZUREAD_TOKEN_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/token";
+    AUTH_AZUREAD_ALLOWED_DOMAINS = cfg.auth.azuread.allowedDomains;
+    AUTH_AZUREAD_ALLOWED_GROUPS = cfg.auth.azuread.allowedGroups;
+    AUTH_AZUREAD_ROLE_ATTRIBUTE_STRICT = false;
+
     AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
     AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
     AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
@@ -109,6 +124,11 @@ let
         default = 1;
         description = "Org id. will default to orgId 1 if not specified.";
       };
+      uid = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically.";
+      };
       url = mkOption {
         type = types.str;
         description = "Url of the datasource.";
@@ -214,6 +234,11 @@ let
           type = types.path;
           description = "Path grafana will watch for dashboards.";
         };
+        foldersFromFilesStructure = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Use folder names from filesystem to create folders in Grafana.";
+        };
       };
     };
   };
@@ -479,6 +504,14 @@ in {
       };
     };
 
+    server = {
+      serveFromSubPath = mkOption {
+        description = "Serve Grafana from subpath specified in rootUrl setting";
+        default = false;
+        type = types.bool;
+      };
+    };
+
     smtp = {
       enable = mkEnableOption "smtp";
       host = mkOption {
@@ -541,6 +574,12 @@ in {
     };
 
     auth = {
+      disableLoginForm = mkOption {
+        description = "Set to true to disable (hide) the login form, useful if you use OAuth";
+        default = false;
+        type = types.bool;
+      };
+
       anonymous = {
         enable = mkOption {
           description = "Whether to allow anonymous access.";
@@ -558,6 +597,53 @@ in {
           type = types.str;
         };
       };
+      azuread = {
+        enable = mkOption {
+          description = "Whether to allow Azure AD OAuth.";
+          default = false;
+          type = types.bool;
+        };
+        allowSignUp = mkOption {
+          description = "Whether to allow sign up with Azure AD OAuth.";
+          default = false;
+          type = types.bool;
+        };
+        clientId = mkOption {
+          description = "Azure AD OAuth client ID.";
+          default = "";
+          type = types.str;
+        };
+        clientSecretFile = mkOption {
+          description = "Azure AD OAuth client secret.";
+          default = null;
+          type = types.nullOr types.path;
+        };
+        tenantId = mkOption {
+          description = ''
+            Tenant id used to create auth and token url. Default to "common"
+            , let user sign in with any tenant.
+            '';
+          default = "common";
+          type = types.str;
+        };
+        allowedDomains = mkOption {
+          description = ''
+            To limit access to authenticated users who are members of one or more groups,
+            set allowedGroups to a comma- or space-separated list of group object IDs.
+            You can find object IDs for a specific group on the Azure portal.
+          '';
+          default = "";
+          type = types.str;
+        };
+        allowedGroups = mkOption {
+          description = ''
+            Limits access to users who belong to specific domains.
+            Separate domains with space or comma.
+          '';
+          default = "";
+          type = types.str;
+        };
+      };
       google = {
         enable = mkOption {
           description = "Whether to allow Google OAuth2.";
@@ -647,6 +733,10 @@ in {
         set -o errexit -o pipefail -o nounset -o errtrace
         shopt -s inherit_errexit
 
+        ${optionalString (cfg.auth.azuread.clientSecretFile != null) ''
+          GF_AUTH_AZUREAD_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.azuread.clientSecretFile})"
+          export GF_AUTH_AZUREAD_CLIENT_SECRET
+        ''}
         ${optionalString (cfg.auth.google.clientSecretFile != null) ''
           GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
           export GF_AUTH_GOOGLE_CLIENT_SECRET
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index baa943302a00f..1dc2cee479ac3 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -24,16 +24,6 @@ let
     + cfg.web.extraConfig
   );
 
-  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:"}
-    ${concatMapStringsSep "\n" (f: "  - " + f.moduleName) cfg.api.finders}
-    ${optionalString (cfg.api.functions != []) "functions:"}
-    ${concatMapStringsSep "\n" (f: "  - " + f) cfg.api.functions}
-    ${cfg.api.extraConfig}
-  '';
-
   seyrenConfig = {
     SEYREN_URL = cfg.seyren.seyrenUrl;
     MONGO_URL = cfg.seyren.mongoUrl;
@@ -72,6 +62,8 @@ let
 in {
 
   imports = [
+    (mkRemovedOptionModule ["services" "graphite" "api"] "")
+    (mkRemovedOptionModule ["services" "graphite" "beacon"] "")
     (mkRemovedOptionModule ["services" "graphite" "pager"] "")
   ];
 
@@ -115,88 +107,6 @@ in {
       };
     };
 
-    api = {
-      enable = mkOption {
-        description = ''
-          Whether to enable graphite api. Graphite api is lightweight alternative
-          to graphite web, with api and without dashboard. It's advised to use
-          grafana as alternative dashboard and influxdb as alternative to
-          graphite carbon.
-
-          For more information visit
-          <link xlink:href="https://graphite-api.readthedocs.org/en/latest/"/>
-        '';
-        default = false;
-        type = types.bool;
-      };
-
-      finders = mkOption {
-        description = "List of finder plugins to load.";
-        default = [];
-        example = literalExpression "[ pkgs.python3Packages.influxgraph ]";
-        type = types.listOf types.package;
-      };
-
-      functions = mkOption {
-        description = "List of functions to load.";
-        default = [
-          "graphite_api.functions.SeriesFunctions"
-          "graphite_api.functions.PieFunctions"
-        ];
-        type = types.listOf types.str;
-      };
-
-      listenAddress = mkOption {
-        description = "Graphite web service listen address.";
-        default = "127.0.0.1";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Graphite api service port.";
-        default = 8080;
-        type = types.int;
-      };
-
-      package = mkOption {
-        description = "Package to use for graphite api.";
-        default = pkgs.python3Packages.graphite_api;
-        defaultText = literalExpression "pkgs.python3Packages.graphite_api";
-        type = types.package;
-      };
-
-      extraConfig = mkOption {
-        description = "Extra configuration for graphite api.";
-        default = ''
-          whisper:
-            directories:
-                - ${dataDir}/whisper
-        '';
-        defaultText = literalExpression ''
-          '''
-            whisper:
-              directories:
-                - ''${config.${opt.dataDir}}/whisper
-          '''
-        '';
-        example = ''
-          allowed_origins:
-            - dashboard.example.com
-          cheat_times: true
-          influxdb:
-            host: localhost
-            port: 8086
-            user: influxdb
-            pass: influxdb
-            db: metrics
-          cache:
-            CACHE_TYPE: 'filesystem'
-            CACHE_DIR: '/tmp/graphite-api-cache'
-        '';
-        type = types.lines;
-      };
-    };
-
     carbon = {
       config = mkOption {
         description = "Content of carbon configuration file.";
@@ -354,16 +264,6 @@ in {
         '';
       };
     };
-
-    beacon = {
-      enable = mkEnableOption "graphite beacon";
-
-      config = mkOption {
-        description = "Graphite beacon configuration.";
-        default = {};
-        type = types.attrs;
-      };
-    };
   };
 
   ###### implementation
@@ -489,44 +389,6 @@ in {
       environment.systemPackages = [ pkgs.python3Packages.graphite-web ];
     }))
 
-    (mkIf cfg.api.enable {
-      systemd.services.graphiteApi = {
-        description = "Graphite Api Interface";
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
-        environment = {
-          PYTHONPATH = let
-              aenv = pkgs.python3.buildEnv.override {
-                extraLibs = [ cfg.api.package pkgs.cairo pkgs.python3Packages.cffi ] ++ cfg.api.finders;
-              };
-            in "${aenv}/${pkgs.python3.sitePackages}";
-          GRAPHITE_API_CONFIG = graphiteApiConfig;
-          LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
-        };
-        serviceConfig = {
-          ExecStart = ''
-            ${pkgs.python3Packages.waitress}/bin/waitress-serve \
-            --host=${cfg.api.listenAddress} --port=${toString cfg.api.port} \
-            graphite_api.app:app
-          '';
-          User = "graphite";
-          Group = "graphite";
-          PermissionsStartOnly = true;
-        };
-        preStart = ''
-          if ! test -e ${dataDir}/db-created; then
-            mkdir -p ${dataDir}/cache/
-            chmod 0700 ${dataDir}/cache/
-
-            chown graphite:graphite ${cfg.dataDir}
-            chown -R graphite:graphite ${cfg.dataDir}/cache
-
-            touch ${dataDir}/db-created
-          fi
-        '';
-      };
-    })
-
     (mkIf cfg.seyren.enable {
       systemd.services.seyren = {
         description = "Graphite Alerting Dashboard";
@@ -550,25 +412,9 @@ in {
       services.mongodb.enable = mkDefault true;
     })
 
-    (mkIf cfg.beacon.enable {
-      systemd.services.graphite-beacon = {
-        description = "Grpahite Beacon Alerting Daemon";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          ExecStart = ''
-            ${pkgs.python3Packages.graphite_beacon}/bin/graphite-beacon \
-              --config=${pkgs.writeText "graphite-beacon.json" (builtins.toJSON cfg.beacon.config)}
-          '';
-          User = "graphite";
-          Group = "graphite";
-        };
-      };
-    })
-
     (mkIf (
       cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay ||
-      cfg.web.enable || cfg.api.enable ||
-      cfg.seyren.enable || cfg.beacon.enable
+      cfg.web.enable || cfg.seyren.enable
      ) {
       users.users.graphite = {
         uid = config.ids.uids.graphite;
diff --git a/nixos/modules/services/monitoring/mimir.nix b/nixos/modules/services/monitoring/mimir.nix
new file mode 100644
index 0000000000000..83c0b23c59dd8
--- /dev/null
+++ b/nixos/modules/services/monitoring/mimir.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) escapeShellArgs mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.mimir;
+
+  settingsFormat = pkgs.formats.yaml {};
+in {
+  options.services.mimir = {
+    enable = mkEnableOption "mimir";
+
+    configuration = mkOption {
+      type = (pkgs.formats.json {}).type;
+      default = {};
+      description = ''
+        Specify the configuration for Mimir in Nix.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Specify a configuration file that Mimir should use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # for mimirtool
+    environment.systemPackages = [ pkgs.mimir ];
+
+    assertions = [{
+      assertion = (
+        (cfg.configuration == {} -> cfg.configFile != null) &&
+        (cfg.configFile != null -> cfg.configuration == {})
+      );
+      message  = ''
+        Please specify either
+        'services.mimir.configuration' or
+        'services.mimir.configFile'.
+      '';
+    }];
+
+    systemd.services.mimir = {
+      description = "mimir Service Daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = let
+        conf = if cfg.configFile == null
+               then settingsFormat.generate "config.yaml" cfg.configuration
+               else cfg.configFile;
+      in
+      {
+        ExecStart = "${pkgs.mimir}/bin/mimir --config.file=${conf}";
+        DynamicUser = true;
+        Restart = "always";
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        WorkingDirectory = "/var/lib/mimir";
+        StateDirectory = "mimir";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 2c7f0ed196688..69173ce4e44ed 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -102,8 +102,8 @@ in
 
       plugins = mkOption {
         type = types.listOf types.package;
-        default = with pkgs; [ monitoring-plugins ssmtp mailutils ];
-        defaultText = literalExpression "[pkgs.monitoring-plugins pkgs.ssmtp pkgs.mailutils]";
+        default = with pkgs; [ monitoring-plugins msmtp mailutils ];
+        defaultText = literalExpression "[pkgs.monitoring-plugins pkgs.msmtp pkgs.mailutils]";
         description = "
           Packages to be added to the Nagios <envar>PATH</envar>.
           Typically used to add plugins, but can be anything.
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index f528d18304244..baf869af1c428 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -186,7 +186,7 @@ in {
       description = "Real time performance monitoring";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = (with pkgs; [ curl gawk iproute2 which procps ])
+      path = (with pkgs; [ curl gawk iproute2 which procps bash ])
         ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages)
         ++ lib.optional config.virtualisation.libvirtd.enable (config.virtualisation.libvirtd.package);
       environment = {
@@ -201,6 +201,10 @@ in {
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c /etc/netdata/netdata.conf";
         ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
+        ExecStartPost = pkgs.writeShellScript "wait-for-netdata-up" ''
+          while [ "$(${pkgs.netdata}/bin/netdatacli ping)" != pong ]; do sleep 0.5; done
+        '';
+
         TimeoutStopSec = 60;
         Restart = "on-failure";
         # User and group
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index ec71365ba3c1c..efc7f69be7d64 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -3,7 +3,19 @@
 let
   cfg = config.services.parsedmarc;
   opt = options.services.parsedmarc;
-  ini = pkgs.formats.ini {};
+  isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+  ini = pkgs.formats.ini {
+    mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec {
+      mkValueString = v:
+        if isInt           v then toString v
+        else if isString   v then v
+        else if true  ==   v then "True"
+        else if false ==   v then "False"
+        else if isSecret   v then hashString "sha256" v._secret
+        else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+    };
+  };
+  inherit (builtins) elem isAttrs isString isInt isList typeOf hashString;
 in
 {
   options.services.parsedmarc = {
@@ -107,11 +119,35 @@ in
     };
 
     settings = lib.mkOption {
+      example = lib.literalExpression ''
+        {
+          imap = {
+            host = "imap.example.com";
+            user = "alice@example.com";
+            password = { _secret = "/run/keys/imap_password" };
+            watch = true;
+          };
+          splunk_hec = {
+            url = "https://splunkhec.example.com";
+            token = { _secret = "/run/keys/splunk_token" };
+            index = "email";
+          };
+        }
+      '';
       description = ''
         Configuration parameters to set in
         <filename>parsedmarc.ini</filename>. For a full list of
         available parameters, see
         <link xlink:href="https://domainaware.github.io/parsedmarc/#configuration-file" />.
+
+        Settings containing secret data should be set to an attribute
+        set containing the attribute <literal>_secret</literal> - a
+        string pointing to a file containing the value the option
+        should be set to. See the example to get a better picture of
+        this: in the resulting <filename>parsedmarc.ini</filename>
+        file, the <literal>splunk_hec.token</literal> key will be set
+        to the contents of the
+        <filename>/run/keys/splunk_token</filename> file.
       '';
 
       type = lib.types.submodule {
@@ -170,11 +206,18 @@ in
             };
 
             password = lib.mkOption {
-              type = with lib.types; nullOr path;
+              type = with lib.types; nullOr (either path (attrsOf path));
               default = null;
               description = ''
-                The path to a file containing the IMAP server password.
+                The IMAP server password.
+
+                Always handled as a secret whether the value is
+                wrapped in a <literal>{ _secret = ...; }</literal>
+                attrset or not (refer to <xref
+                linkend="opt-services.parsedmarc.settings" /> for
+                details).
               '';
+              apply = x: if isAttrs x || x == null then x else { _secret = x; };
             };
 
             watch = lib.mkOption {
@@ -228,11 +271,18 @@ in
             };
 
             password = lib.mkOption {
-              type = with lib.types; nullOr path;
+              type = with lib.types; nullOr (either path (attrsOf path));
               default = null;
               description = ''
-                The path to a file containing the SMTP server password.
+                The SMTP server password.
+
+                Always handled as a secret whether the value is
+                wrapped in a <literal>{ _secret = ...; }</literal>
+                attrset or not (refer to <xref
+                linkend="opt-services.parsedmarc.settings" /> for
+                details).
               '';
+              apply = x: if isAttrs x || x == null then x else { _secret = x; };
             };
 
             from = lib.mkOption {
@@ -274,12 +324,19 @@ in
             };
 
             password = lib.mkOption {
-              type = with lib.types; nullOr path;
+              type = with lib.types; nullOr (either path (attrsOf path));
               default = null;
               description = ''
-                The path to a file containing the password to use when
-                connecting to Elasticsearch, if required.
+                The password to use when connecting to Elasticsearch,
+                if required.
+
+                Always handled as a secret whether the value is
+                wrapped in a <literal>{ _secret = ...; }</literal>
+                attrset or not (refer to <xref
+                linkend="opt-services.parsedmarc.settings" /> for
+                details).
               '';
+              apply = x: if isAttrs x || x == null then x else { _secret = x; };
             };
 
             ssl = lib.mkOption {
@@ -299,63 +356,6 @@ in
               '';
             };
           };
-
-          kafka = {
-            hosts = lib.mkOption {
-              default = [];
-              type = with lib.types; listOf str;
-              apply = x: if x == [] then null else lib.concatStringsSep "," x;
-              description = ''
-                A list of Apache Kafka hosts to publish parsed reports
-                to.
-              '';
-            };
-
-            user = lib.mkOption {
-              type = with lib.types; nullOr str;
-              default = null;
-              description = ''
-                Username to use when connecting to Kafka, if
-                required.
-              '';
-            };
-
-            password = lib.mkOption {
-              type = with lib.types; nullOr path;
-              default = null;
-              description = ''
-                The path to a file containing the password to use when
-                connecting to Kafka, if required.
-              '';
-            };
-
-            ssl = lib.mkOption {
-              type = with lib.types; nullOr bool;
-              default = null;
-              description = ''
-                Whether to use an encrypted SSL/TLS connection.
-              '';
-            };
-
-            aggregate_topic = lib.mkOption {
-              type = with lib.types; nullOr str;
-              default = null;
-              example = "aggregate";
-              description = ''
-                The Kafka topic to publish aggregate reports on.
-              '';
-            };
-
-            forensic_topic = lib.mkOption {
-              type = with lib.types; nullOr str;
-              default = null;
-              example = "forensic";
-              description = ''
-                The Kafka topic to publish forensic reports on.
-              '';
-            };
-          };
-
         };
 
       };
@@ -404,21 +404,14 @@ in
         enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
         datasources =
           let
-            pkgVer = lib.getVersion config.services.elasticsearch.package;
-            esVersion =
-              if lib.versionOlder pkgVer "7" then
-                "60"
-              else if lib.versionOlder pkgVer "8" then
-                "70"
-              else
-                throw "When provisioning parsedmarc grafana datasources: unknown Elasticsearch version.";
+            esVersion = lib.getVersion config.services.elasticsearch.package;
           in
             lib.mkIf cfg.provision.grafana.datasource [
               {
                 name = "dmarc-ag";
                 type = "elasticsearch";
                 access = "proxy";
-                url = "localhost:9200";
+                url = "http://localhost:9200";
                 jsonData = {
                   timeField = "date_range";
                   inherit esVersion;
@@ -428,7 +421,7 @@ in
                 name = "dmarc-fo";
                 type = "elasticsearch";
                 access = "proxy";
-                url = "localhost:9200";
+                url = "http://localhost:9200";
                 jsonData = {
                   timeField = "date_range";
                   inherit esVersion;
@@ -467,12 +460,17 @@ in
         # lists, empty attrsets and null. This makes it possible to
         # list interesting options in `settings` without them always
         # ending up in the resulting config.
-        filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [ null [] {} ])) cfg.settings;
+        filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null [] {} ])) cfg.settings;
+
+        # Extract secrets (attributes set to an attrset with a
+        # "_secret" key) from the settings and generate the commands
+        # to run to perform the secret replacements.
+        secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
         parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig;
-        mkSecretReplacement = file:
-          lib.optionalString (file != null) ''
-            replace-secret '${file}' '${file}' /run/parsedmarc/parsedmarc.ini
-          '';
+        mkSecretReplacement = file: ''
+          replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/parsedmarc/parsedmarc.ini" ]}
+        '';
+        secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
       in
         {
           wantedBy = [ "multi-user.target" ];
@@ -487,10 +485,7 @@ in
                 umask u=rwx,g=,o=
                 cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
                 chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
-                ${mkSecretReplacement cfg.settings.smtp.password}
-                ${mkSecretReplacement cfg.settings.imap.password}
-                ${mkSecretReplacement cfg.settings.elasticsearch.password}
-                ${mkSecretReplacement cfg.settings.kafka.password}
+                ${secretReplacements}
               '' + lib.optionalString cfg.provision.localMail.enable ''
                 openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd
                 replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index f563861b61c08..41848c1c6d3a7 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -3,7 +3,11 @@
 with lib;
 
 let
+  json = pkgs.formats.json { };
   cfg = config.services.prometheus;
+  checkConfigEnabled =
+    (lib.isBool cfg.checkConfig && cfg.checkConfig)
+      || cfg.checkConfig == "syntax-only";
 
   workingDir = "/var/lib/" + cfg.stateDir;
 
@@ -26,7 +30,7 @@ let
 
   # a wrapper that verifies that the configuration is valid
   promtoolCheck = what: name: file:
-    if cfg.checkConfig then
+    if checkConfigEnabled then
       pkgs.runCommandLocal
         "${name}-${replaceStrings [" "] [""] what}-checked"
         { buildInputs = [ cfg.package ]; } ''
@@ -34,13 +38,7 @@ let
         promtool ${what} $out
       '' else file;
 
-  # Pretty-print JSON to a file
-  writePrettyJSON = name: x:
-    pkgs.runCommandLocal name { } ''
-      echo '${builtins.toJSON x}' | ${pkgs.jq}/bin/jq . > $out
-    '';
-
-  generatedPrometheusYml = writePrettyJSON "prometheus.yml" promConfig;
+  generatedPrometheusYml = json.generate "prometheus.yml" promConfig;
 
   # This becomes the main config file for Prometheus
   promConfig = {
@@ -63,7 +61,7 @@ let
           pkgs.writeText "prometheus.yml" cfg.configText
         else generatedPrometheusYml;
     in
-    promtoolCheck "check config" "prometheus.yml" yml;
+    promtoolCheck "check config ${lib.optionalString (cfg.checkConfig == "syntax-only") "--syntax-only"}" "prometheus.yml" yml;
 
   cmdlineArgs = cfg.extraFlags ++ [
     "--storage.tsdb.path=${workingDir}/data/"
@@ -74,7 +72,6 @@ let
     }"
     "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}"
     "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}"
-    "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s"
   ] ++ optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}"
     ++ optional (cfg.retentionTime != null) "--storage.tsdb.retention.time=${cfg.retentionTime}";
 
@@ -1563,6 +1560,8 @@ in
     (mkRenamedOptionModule [ "services" "prometheus2" ] [ "services" "prometheus" ])
     (mkRemovedOptionModule [ "services" "prometheus" "environmentFile" ]
       "It has been removed since it was causing issues (https://github.com/NixOS/nixpkgs/issues/126083) and Prometheus now has native support for secret files, i.e. `basic_auth.password_file` and `authorization.credentials_file`.")
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanagerTimeout" ]
+      "Deprecated upstream and no longer had any effect")
   ];
 
   options.services.prometheus = {
@@ -1719,14 +1718,6 @@ in
       '';
     };
 
-    alertmanagerTimeout = mkOption {
-      type = types.int;
-      default = 10;
-      description = ''
-        Alert manager HTTP API timeout (in seconds).
-      '';
-    };
-
     webExternalUrl = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -1738,16 +1729,20 @@ in
     };
 
     checkConfig = mkOption {
-      type = types.bool;
+      type = with types; either bool (enum [ "syntax-only" ]);
       default = true;
+      example = "syntax-only";
       description = ''
         Check configuration with <literal>promtool
         check</literal>. The call to <literal>promtool</literal> is
-        subject to sandboxing by Nix. When credentials are stored in
-        external files (<literal>password_file</literal>,
-        <literal>bearer_token_file</literal>, etc), they will not be
-        visible to <literal>promtool</literal> and it will report
-        errors, despite a correct configuration.
+        subject to sandboxing by Nix.
+
+        If you use credentials stored in external files
+        (<literal>password_file</literal>, <literal>bearer_token_file</literal>, etc),
+        they will not be visible to <literal>promtool</literal>
+        and it will report errors, despite a correct configuration.
+        To resolve this, you may set this option to <literal>"syntax-only"</literal>
+        in order to only syntax check the Prometheus configuration.
       '';
     };
 
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index d29d50706ef60..41302d6d3ceb3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -29,6 +29,7 @@ let
     "blackbox"
     "buildkite-agent"
     "collectd"
+    "dmarc"
     "dnsmasq"
     "domain"
     "dovecot"
@@ -55,6 +56,7 @@ let
     "postfix"
     "postgres"
     "process"
+    "pve"
     "py-air-control"
     "redis"
     "rspamd"
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.xml b/nixos/modules/services/monitoring/prometheus/exporters.xml
index c2d4b05996a4b..1df88bb61a12d 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.xml
+++ b/nixos/modules/services/monitoring/prometheus/exporters.xml
@@ -19,6 +19,7 @@
 <programlisting>
   services.prometheus.exporters.node = {
     enable = true;
+    port = 9100;
     enabledCollectors = [
       "logind"
       "systemd"
@@ -42,6 +43,26 @@
    <link xlink:href="https://nixos.org/nixos/options.html#prometheus.exporters">available
    options</link>.
   </para>
+
+  <para>
+    Prometheus can now be configured to consume the metrics produced by the exporter:
+    <programlisting>
+    services.prometheus = {
+      # ...
+
+      scrapeConfigs = [
+        {
+          job_name = "node";
+          static_configs = [{
+            targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
+          }];
+        }
+      ];
+
+      # ...
+    }
+    </programlisting>
+  </para>
  </section>
  <section xml:id="module-services-prometheus-exporters-new-exporter">
   <title>Adding a new exporter</title>
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
index 1ef264fc86e5a..5fda4c980ebb8 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
@@ -17,7 +17,7 @@ in
     };
     birdSocket = mkOption {
       type = types.path;
-      default = "/var/run/bird.ctl";
+      default = "/run/bird/bird.ctl";
       description = ''
         Path to BIRD2 (or BIRD1 v4) socket.
       '';
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix b/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix
new file mode 100644
index 0000000000000..25950e1ece967
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.dmarc;
+
+  json = builtins.toJSON {
+    inherit (cfg) folders port;
+    listen_addr = cfg.listenAddress;
+    storage_path = "$STATE_DIRECTORY";
+    imap = (builtins.removeAttrs cfg.imap [ "passwordFile" ]) // { password = "$IMAP_PASSWORD"; use_ssl = true; };
+    poll_interval_seconds = cfg.pollIntervalSeconds;
+    deduplication_max_seconds = cfg.deduplicationMaxSeconds;
+    logging = {
+      version = 1;
+      disable_existing_loggers = false;
+    };
+  };
+in {
+  port = 9797;
+  extraOpts = {
+    imap = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Hostname of IMAP server to connect to.
+        '';
+      };
+      port = mkOption {
+        type = types.port;
+        default = 993;
+        description = ''
+          Port of the IMAP server to connect to.
+        '';
+      };
+      username = mkOption {
+        type = types.str;
+        example = "postmaster@example.org";
+        description = ''
+          Login username for the IMAP connection.
+        '';
+      };
+      passwordFile = mkOption {
+        type = types.str;
+        example = "/run/secrets/dovecot_pw";
+        description = ''
+          File containing the login password for the IMAP connection.
+        '';
+      };
+    };
+    folders = {
+      inbox = mkOption {
+        type = types.str;
+        default = "INBOX";
+        description = ''
+          IMAP mailbox that is checked for incoming DMARC aggregate reports
+        '';
+      };
+      done = mkOption {
+        type = types.str;
+        default = "Archive";
+        description = ''
+          IMAP mailbox that successfully processed reports are moved to.
+        '';
+      };
+      error = mkOption {
+        type = types.str;
+        default = "Invalid";
+        description = ''
+          IMAP mailbox that emails are moved to that could not be processed.
+        '';
+      };
+    };
+    pollIntervalSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 60;
+      description = ''
+        How often to poll the IMAP server in seconds.
+      '';
+    };
+    deduplicationMaxSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 604800;
+      defaultText = "7 days (in seconds)";
+      description = ''
+        How long individual report IDs will be remembered to avoid
+        counting double delivered reports twice.
+      '';
+    };
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to declare enable <literal>--debug</literal>.
+      '';
+    };
+  };
+  serviceOpts = {
+    path = with pkgs; [ envsubst coreutils ];
+    serviceConfig = {
+      StateDirectory = "prometheus-dmarc-exporter";
+      WorkingDirectory = "/var/lib/prometheus-dmarc-exporter";
+      ExecStart = "${pkgs.writeShellScript "setup-cfg" ''
+        export IMAP_PASSWORD="$(<${cfg.imap.passwordFile})"
+        envsubst \
+          -i ${pkgs.writeText "dmarc-exporter.json.template" json} \
+          -o ''${STATE_DIRECTORY}/dmarc-exporter.json
+
+        exec ${pkgs.dmarc-metrics-exporter}/bin/dmarc-metrics-exporter \
+          --configuration /var/lib/prometheus-dmarc-exporter/dmarc-exporter.json \
+          ${optionalString cfg.debug "--debug"}
+      ''}";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
index 27aeb9096243c..e0ee90d9b97db 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
@@ -25,6 +25,10 @@ in {
     };
   };
   serviceOpts = {
+    after = [
+      "kea-dhcp4-server.service"
+      "kea-dhcp6-server.service"
+    ];
     serviceConfig = {
       User = "kea";
       ExecStart = ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
index 956bd96aa4543..a60f47f63932a 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
@@ -5,6 +5,8 @@ with lib;
 let
   cfg = config.services.prometheus.exporters.mail;
 
+  configFile = if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configFile);
+
   configurationFile = pkgs.writeText "prometheus-mail-exporter.conf" (builtins.toJSON (
     # removes the _module attribute, null values and converts attrNames to lowercase
     mapAttrs' (name: value:
@@ -137,6 +139,13 @@ in
 {
   port = 9225;
   extraOpts = {
+    environmentFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        File containing env-vars to be substituted into the exporter's config.
+      '';
+    };
     configFile = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -162,13 +171,19 @@ in
   serviceOpts = {
     serviceConfig = {
       DynamicUser = false;
+      EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+      RuntimeDirectory = "prometheus-mail-exporter";
+      ExecStartPre = [
+        "${pkgs.writeShellScript "subst-secrets-mail-exporter" ''
+          umask 0077
+          ${pkgs.envsubst}/bin/envsubst -i ${configFile} -o ''${RUNTIME_DIRECTORY}/mail-exporter.json
+        ''}"
+      ];
       ExecStart = ''
         ${pkgs.prometheus-mail-exporter}/bin/mailexporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           --web.telemetry-path ${cfg.telemetryPath} \
-          --config.file ${
-            if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configFile)
-          } \
+          --config.file ''${RUNTIME_DIRECTORY}/mail-exporter.json \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/node.nix b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
index 5e5fc7cd55245..417920402f341 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/node.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
@@ -44,6 +44,8 @@ in
       ];
       # The timex collector needs to access clock APIs
       ProtectClock = any (collector: collector == "timex") cfg.disabledCollectors;
+      # Allow space monitoring under /home
+      ProtectHome = true;
     };
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
index 4d3c1fa267e5f..53509b7a385b4 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
@@ -74,11 +74,13 @@ in
     };
   };
   serviceOpts = {
+    after = mkIf cfg.systemd.enable [ cfg.systemd.unit ];
     serviceConfig = {
       DynamicUser = false;
       # By default, each prometheus exporter only gets AF_INET & AF_INET6,
       # but AF_UNIX is needed to read from the `showq`-socket.
       RestrictAddressFamilies = [ "AF_UNIX" ];
+      SupplementaryGroups = mkIf cfg.systemd.enable [ "systemd-journal" ];
       ExecStart = ''
         ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pve.nix b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix
new file mode 100644
index 0000000000000..ef708414c95ef
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+let
+  cfg = config.services.prometheus.exporters.pve;
+
+  # pve exporter requires a config file so create an empty one if configFile is not provided
+  emptyConfigFile = pkgs.writeTextFile {
+    name = "pve.yml";
+    text = "default:";
+  };
+
+  computedConfigFile = "${if cfg.configFile == null then emptyConfigFile else cfg.configFile}";
+in
+{
+  port = 9221;
+  extraOpts = {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.prometheus-pve-exporter;
+      defaultText = literalExpression "pkgs.prometheus-pve-exporter";
+      example = literalExpression "pkgs.prometheus-pve-exporter";
+      description = ''
+        The package to use for prometheus-pve-exporter
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/etc/prometheus-pve-exporter/pve.env";
+      description = ''
+        Path to the service's environment file. This path can either be a computed path in /nix/store or a path in the local filesystem.
+
+        The environment file should NOT be stored in /nix/store as it contains passwords and/or keys in plain text.
+
+        Environment reference: https://github.com/prometheus-pve/prometheus-pve-exporter#authentication
+      '';
+    };
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/etc/prometheus-pve-exporter/pve.yml";
+      description = ''
+        Path to the service's config file. This path can either be a computed path in /nix/store or a path in the local filesystem.
+
+        The config file should NOT be stored in /nix/store as it will contain passwords and/or keys in plain text.
+
+        If both configFile and environmentFile are provided, the configFile option will be ignored.
+
+        Configuration reference: https://github.com/prometheus-pve/prometheus-pve-exporter/#authentication
+      '';
+    };
+
+    collectors = {
+      status = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect Node/VM/CT status
+        '';
+      };
+      version = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE version info
+        '';
+      };
+      node = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE node info
+        '';
+      };
+      cluster = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE cluster info
+        '';
+      };
+      resources = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE resources info
+        '';
+      };
+      config = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE onboot status
+        '';
+      };
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${cfg.package}/bin/pve_exporter \
+          --${if cfg.collectors.status == true then "" else "no-"}collector.status \
+          --${if cfg.collectors.version == true then "" else "no-"}collector.version \
+          --${if cfg.collectors.node == true then "" else "no-"}collector.node \
+          --${if cfg.collectors.cluster == true then "" else "no-"}collector.cluster \
+          --${if cfg.collectors.resources == true then "" else "no-"}collector.resources \
+          --${if cfg.collectors.config == true then "" else "no-"}collector.config \
+          ${computedConfigFile} \
+          ${toString cfg.port} ${cfg.listenAddress}
+      '';
+    } // optionalAttrs (cfg.environmentFile != null) {
+          EnvironmentFile = cfg.environmentFile;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
index c0a50f07d7171..2edd1de83e1bf 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
@@ -11,7 +11,7 @@ in {
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-systemd-exporter}/bin/systemd_exporter \
-          --web.listen-address ${cfg.listenAddress}:${toString cfg.port}
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
       '';
       RestrictAddressFamilies = [
         # Need AF_UNIX to collect data
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
index 5b5a6e18fcd65..ede6028933a4d 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
@@ -45,7 +45,8 @@ in
     };
     instance = mkOption {
       type = types.nullOr types.str;
-      default = null;
+      default = config.services.varnish.stateDir;
+      defaultText = lib.literalExpression "config.services.varnish.stateDir";
       description = ''
         varnishstat -n value.
       '';
@@ -66,7 +67,7 @@ in
     };
   };
   serviceOpts = {
-    path = [ pkgs.varnish ];
+    path = [ config.services.varnish.package ];
     serviceConfig = {
       RestartSec = mkDefault 1;
       DynamicUser = false;
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
index d4aa69629ec89..2d329a1af1cbc 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
@@ -57,9 +57,9 @@ in {
         ${pkgs.prometheus-wireguard-exporter}/bin/prometheus_wireguard_exporter \
           -p ${toString cfg.port} \
           -l ${cfg.listenAddress} \
-          ${optionalString cfg.verbose "-v"} \
-          ${optionalString cfg.singleSubnetPerField "-s"} \
-          ${optionalString cfg.withRemoteIp "-r"} \
+          ${optionalString cfg.verbose "-v true"} \
+          ${optionalString cfg.singleSubnetPerField "-s true"} \
+          ${optionalString cfg.withRemoteIp "-r true"} \
           ${optionalString (cfg.wireguardConfig != null) "-n ${escapeShellArg cfg.wireguardConfig}"}
       '';
       RestrictAddressFamilies = [
diff --git a/nixos/modules/services/network-filesystems/glusterfs.nix b/nixos/modules/services/network-filesystems/glusterfs.nix
index 38be098de5d90..aa5dd91d55370 100644
--- a/nixos/modules/services/network-filesystems/glusterfs.nix
+++ b/nixos/modules/services/network-filesystems/glusterfs.nix
@@ -159,9 +159,10 @@ in
         install -m 0755 -d /var/log/glusterfs
       ''
       # The copying of hooks is due to upstream bug https://bugzilla.redhat.com/show_bug.cgi?id=1452761
+      # Excludes one hook due to missing SELinux binaries.
       + ''
         mkdir -p /var/lib/glusterd/hooks/
-        ${rsync}/bin/rsync -a ${glusterfs}/var/lib/glusterd/hooks/ /var/lib/glusterd/hooks/
+        ${rsync}/bin/rsync -a --exclude="S10selinux-label-brick.sh" ${glusterfs}/var/lib/glusterd/hooks/ /var/lib/glusterd/hooks/
 
         ${tlsCmd}
       ''
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index b311b91b4a033..b7e6a787cfb1b 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -1,16 +1,16 @@
-{ config, lib, pkgs, options, ... }:
+{ config, lib, pkgs, utils, ... }:
 with lib;
 let
   cfg = config.services.ipfs;
-  opt = options.services.ipfs;
 
-  ipfsFlags = toString ([
-    (optionalString cfg.autoMount "--mount")
-    (optionalString cfg.enableGC "--enable-gc")
-    (optionalString (cfg.serviceFdlimit != null) "--manage-fdlimit=false")
-    (optionalString (cfg.defaultMode == "offline") "--offline")
-    (optionalString (cfg.defaultMode == "norouting") "--routing=none")
-  ] ++ cfg.extraFlags);
+  ipfsFlags = utils.escapeSystemdExecArgs (
+    optional cfg.autoMount "--mount" ++
+    optional cfg.enableGC "--enable-gc" ++
+    optional (cfg.serviceFdlimit != null) "--manage-fdlimit=false" ++
+    optional (cfg.defaultMode == "offline") "--offline" ++
+    optional (cfg.defaultMode == "norouting") "--routing=none" ++
+    cfg.extraFlags
+  );
 
   profile =
     if cfg.localDiscovery
@@ -239,7 +239,10 @@ in
       "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
-    systemd.packages = [ cfg.package ];
+    # The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself
+    systemd.packages = if cfg.autoMount
+      then [ cfg.package.systemd_unit ]
+      else [ cfg.package.systemd_unit_hardened ];
 
     systemd.services.ipfs = {
       path = [ "/run/wrappers" cfg.package ];
@@ -251,23 +254,27 @@ in
         else
           # After an unclean shutdown this file may exist which will cause the config command to attempt to talk to the daemon. This will hang forever if systemd is holding our sockets open.
           rm -vf "$IPFS_PATH/api"
-
-          ipfs --offline config profile apply ${profile}
+      '' + optionalString cfg.autoMigrate ''
+        ${pkgs.ipfs-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
+      '' + ''
+          ipfs --offline config profile apply ${profile} >/dev/null
         fi
       '' + optionalString cfg.autoMount ''
         ipfs --offline config Mounts.FuseAllowOther --json true
         ipfs --offline config Mounts.IPFS ${cfg.ipfsMountDir}
         ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
-      '' + optionalString cfg.autoMigrate ''
-        ${pkgs.ipfs-migrator}/bin/fs-repo-migrations -y
       '' + ''
         ipfs --offline config show \
           | ${pkgs.jq}/bin/jq '. * $extraConfig' --argjson extraConfig ${
-              escapeShellArg (builtins.toJSON ({
-                Addresses.API = cfg.apiAddress;
-                Addresses.Gateway = cfg.gatewayAddress;
-                Addresses.Swarm = cfg.swarmAddress;
-              } // cfg.extraConfig))
+              escapeShellArg (builtins.toJSON (
+                recursiveUpdate
+                  {
+                    Addresses.API = cfg.apiAddress;
+                    Addresses.Gateway = cfg.gatewayAddress;
+                    Addresses.Swarm = cfg.swarmAddress;
+                  }
+                  cfg.extraConfig
+              ))
             } \
           | ipfs --offline config replace -
       '';
@@ -275,6 +282,8 @@ in
         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}" ];
         User = cfg.user;
         Group = cfg.group;
+        StateDirectory = "";
+        ReadWritePaths = optionals (!cfg.autoMount) [ "" cfg.dataDir ];
       } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
     } // optionalAttrs (!cfg.startWhenNeeded) {
       wantedBy = [ "default.target" ];
@@ -306,6 +315,9 @@ in
         in
         [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
     };
+  };
 
+  meta = {
+    maintainers = with lib.maintainers; [ Luflosi ];
   };
 }
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index 9ed755d0465c4..992f948e8cd53 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -224,6 +224,7 @@ in
           targets.samba = {
             description = "Samba Server";
             after = [ "network.target" ];
+            wants = [ "network-online.target" ];
             wantedBy = [ "multi-user.target" ];
           };
           # Refer to https://github.com/samba-team/samba/tree/master/packaging/systemd
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
index e55ac7a6b18b7..aa72a047526b0 100644
--- a/nixos/modules/services/networking/amuled.nix
+++ b/nixos/modules/services/networking/amuled.nix
@@ -76,7 +76,7 @@ in
 
       script = ''
         ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${user} \
-            -c 'HOME="${cfg.dataDir}" ${pkgs.amuleDaemon}/bin/amuled'
+            -c 'HOME="${cfg.dataDir}" ${pkgs.amule-daemon}/bin/amuled'
       '';
     };
   };
diff --git a/nixos/modules/services/networking/asterisk.nix b/nixos/modules/services/networking/asterisk.nix
index af091d55c01b8..297d0b3b2d02b 100644
--- a/nixos/modules/services/networking/asterisk.nix
+++ b/nixos/modules/services/networking/asterisk.nix
@@ -14,28 +14,9 @@ let
 
   # Add filecontents from files of useTheseDefaultConfFiles to confFiles, do not override
   defaultConfFiles = subtractLists (attrNames cfg.confFiles) cfg.useTheseDefaultConfFiles;
-  allConfFiles =
-    cfg.confFiles //
-    builtins.listToAttrs (map (x: { name = x;
-                                    value = builtins.readFile (cfg.package + "/etc/asterisk/" + x); })
-                              defaultConfFiles);
-
-  asteriskEtc = pkgs.stdenv.mkDerivation
-  ((mapAttrs' (name: value: nameValuePair
-        # Fudge the names to make bash happy
-        ((replaceChars ["."] ["_"] name) + "_")
-        (value)
-      ) allConfFiles) //
-  {
-    confFilesString = concatStringsSep " " (
-      attrNames allConfFiles
-    );
-
-    name = "asterisk-etc";
-
+  allConfFiles = {
     # Default asterisk.conf file
-    # (Notice that astetcdir will be set to the path of this derivation)
-    asteriskConf = ''
+    "asterisk.conf".text = ''
       [directories]
       astetcdir => /etc/asterisk
       astmoddir => ${cfg.package}/lib/asterisk/modules
@@ -48,43 +29,28 @@ let
       astrundir => /run/asterisk
       astlogdir => /var/log/asterisk
       astsbindir => ${cfg.package}/sbin
+      ${cfg.extraConfig}
     '';
-    extraConf = cfg.extraConfig;
 
     # Loading all modules by default is considered sensible by the authors of
     # "Asterisk: The Definitive Guide". Secure sites will likely want to
     # specify their own "modules.conf" in the confFiles option.
-    modulesConf = ''
+    "modules.conf".text = ''
       [modules]
       autoload=yes
     '';
 
     # Use syslog for logging so logs can be viewed with journalctl
-    loggerConf = ''
+    "logger.conf".text = ''
       [general]
 
       [logfiles]
       syslog.local0 => notice,warning,error
     '';
+  } //
+    mapAttrs (name: text: { inherit text; }) cfg.confFiles //
+    listToAttrs (map (x: nameValuePair x { source = cfg.package + "/etc/asterisk/" + x; }) defaultConfFiles);
 
-    buildCommand = ''
-      mkdir -p "$out"
-
-      # Create asterisk.conf, pointing astetcdir to the path of this derivation
-      echo "$asteriskConf" | sed "s|@out@|$out|g" > "$out"/asterisk.conf
-      echo "$extraConf" >> "$out"/asterisk.conf
-
-      echo "$modulesConf" > "$out"/modules.conf
-
-      echo "$loggerConf" > "$out"/logger.conf
-
-      # Config files specified in confFiles option override all other files
-      for i in $confFilesString; do
-        conf=$(echo "$i"_ | sed 's/\./_/g')
-        echo "''${!conf}" > "$out"/"$i"
-      done
-    '';
-  });
 in
 
 {
@@ -209,7 +175,9 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
-    environment.etc.asterisk.source = asteriskEtc;
+    environment.etc = mapAttrs' (name: value:
+      nameValuePair "asterisk/${name}" value
+    ) allConfFiles;
 
     users.users.asterisk =
       { name = asteriskUser;
diff --git a/nixos/modules/services/networking/bird-lg.nix b/nixos/modules/services/networking/bird-lg.nix
new file mode 100644
index 0000000000000..515ef38608b4b
--- /dev/null
+++ b/nixos/modules/services/networking/bird-lg.nix
@@ -0,0 +1,269 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bird-lg;
+in
+{
+  options = {
+    services.bird-lg = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bird-lg;
+        defaultText = literalExpression "pkgs.bird-lg";
+        description = "The Bird Looking Glass package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bird-lg";
+        description = "User to run the service.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bird-lg";
+        description = "Group to run the service.";
+      };
+
+      frontend = {
+        enable = mkEnableOption "Bird Looking Glass Frontend Webserver";
+
+        listenAddress = mkOption {
+          type = types.str;
+          default = "127.0.0.1:5000";
+          description = "Address to listen on.";
+        };
+
+        proxyPort = mkOption {
+          type = types.port;
+          default = 8000;
+          description = "Port bird-lg-proxy is running on.";
+        };
+
+        domain = mkOption {
+          type = types.str;
+          default = "";
+          example = "dn42.lantian.pub";
+          description = "Server name domain suffixes.";
+        };
+
+        servers = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [ "gigsgigscloud" "hostdare" ];
+          description = "Server name prefixes.";
+        };
+
+        whois = mkOption {
+          type = types.str;
+          default = "whois.verisign-grs.com";
+          description = "Whois server for queries.";
+        };
+
+        dnsInterface = mkOption {
+          type = types.str;
+          default = "asn.cymru.com";
+          description = "DNS zone to query ASN information.";
+        };
+
+        bgpMapInfo = mkOption {
+          type = types.listOf types.str;
+          default = [ "asn" "as-name" "ASName" "descr" ];
+          description = "Information displayed in bgpmap.";
+        };
+
+        titleBrand = mkOption {
+          type = types.str;
+          default = "Bird-lg Go";
+          description = "Prefix of page titles in browser tabs.";
+        };
+
+        netSpecificMode = mkOption {
+          type = types.str;
+          default = "";
+          example = "dn42";
+          description = "Apply network-specific changes for some networks.";
+        };
+
+        protocolFilter = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [ "ospf" ];
+          description = "Information displayed in bgpmap.";
+        };
+
+        nameFilter = mkOption {
+          type = types.str;
+          default = "";
+          example = "^ospf";
+          description = "Protocol names to hide in summary tables (RE2 syntax),";
+        };
+
+        timeout = mkOption {
+          type = types.int;
+          default = 120;
+          description = "Time before request timed out, in seconds.";
+        };
+
+        navbar = {
+          brand = mkOption {
+            type = types.str;
+            default = "Bird-lg Go";
+            description = "Brand to show in the navigation bar .";
+          };
+
+          brandURL = mkOption {
+            type = types.str;
+            default = "/";
+            description = "URL of the brand to show in the navigation bar.";
+          };
+
+          allServers = mkOption {
+            type = types.str;
+            default = "ALL Servers";
+            description = "Text of 'All server' button in the navigation bar.";
+          };
+
+          allServersURL = mkOption {
+            type = types.str;
+            default = "all";
+            description = "URL of 'All servers' button.";
+          };
+        };
+
+        extraArgs = mkOption {
+          type = types.lines;
+          default = "";
+          description = "
+            Extra parameters documented <link xlink:href=\"https://github.com/xddxdd/bird-lg-go#frontend\">here</link>.
+          ";
+        };
+      };
+
+      proxy = {
+        enable = mkEnableOption "Bird Looking Glass Proxy";
+
+        listenAddress = mkOption {
+          type = types.str;
+          default = "127.0.0.1:8000";
+          description = "Address to listen on.";
+        };
+
+        allowedIPs = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [ "192.168.25.52" "192.168.25.53" ];
+          description = "List of IPs to allow (default all allowed).";
+        };
+
+        birdSocket = mkOption {
+          type = types.str;
+          default = "/run/bird.ctl";
+          example = "/var/run/bird/bird.ctl";
+          description = "Bird control socket path.";
+        };
+
+        traceroute = {
+          binary = mkOption {
+            type = types.str;
+            default = "${pkgs.traceroute}/bin/traceroute";
+            defaultText = literalExpression ''"''${pkgs.traceroute}/bin/traceroute"'';
+            description = "Traceroute's binary path.";
+          };
+
+          rawOutput = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Display traceroute output in raw format.";
+          };
+        };
+
+        extraArgs = mkOption {
+          type = types.lines;
+          default = "";
+          description = "
+            Extra parameters documented <link xlink:href=\"https://github.com/xddxdd/bird-lg-go#proxy\">here</link>.
+          ";
+        };
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = {
+    systemd.services = {
+      bird-lg-frontend = mkIf cfg.frontend.enable {
+        enable = true;
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        description = "Bird Looking Glass Frontend Webserver";
+        serviceConfig = {
+          Type = "simple";
+          Restart = "on-failure";
+          ProtectSystem = "full";
+          ProtectHome = "yes";
+          MemoryDenyWriteExecute = "yes";
+          User = cfg.user;
+          Group = cfg.group;
+        };
+        script = ''
+          ${cfg.package}/bin/frontend \
+            --servers ${concatStringsSep "," cfg.frontend.servers } \
+            --domain ${cfg.frontend.domain} \
+            --listen ${cfg.frontend.listenAddress} \
+            --proxy-port ${toString cfg.frontend.proxyPort} \
+            --whois ${cfg.frontend.whois} \
+            --dns-interface ${cfg.frontend.dnsInterface} \
+            --bgpmap-info ${concatStringsSep "," cfg.frontend.bgpMapInfo } \
+            --title-brand ${cfg.frontend.titleBrand} \
+            --navbar-brand ${cfg.frontend.navbar.brand} \
+            --navbar-brand-url ${cfg.frontend.navbar.brandURL} \
+            --navbar-all-servers ${cfg.frontend.navbar.allServers} \
+            --navbar-all-url ${cfg.frontend.navbar.allServersURL} \
+            --net-specific-mode ${cfg.frontend.netSpecificMode} \
+            --protocol-filter ${concatStringsSep "," cfg.frontend.protocolFilter } \
+            --name-filter ${cfg.frontend.nameFilter} \
+            --time-out ${toString cfg.frontend.timeout} \
+            ${cfg.frontend.extraArgs}
+        '';
+      };
+
+      bird-lg-proxy = mkIf cfg.proxy.enable {
+        enable = true;
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        description = "Bird Looking Glass Proxy";
+        serviceConfig = {
+          Type = "simple";
+          Restart = "on-failure";
+          ProtectSystem = "full";
+          ProtectHome = "yes";
+          MemoryDenyWriteExecute = "yes";
+          User = cfg.user;
+          Group = cfg.group;
+        };
+        script = ''
+          ${cfg.package}/bin/proxy \
+          --allowed ${concatStringsSep "," cfg.proxy.allowedIPs } \
+          --bird ${cfg.proxy.birdSocket} \
+          --listen ${cfg.proxy.listenAddress} \
+          --traceroute_bin ${cfg.proxy.traceroute.binary}
+          --traceroute_raw ${boolToString cfg.proxy.traceroute.rawOutput}
+          ${cfg.proxy.extraArgs}
+        '';
+      };
+    };
+    users = mkIf (cfg.frontend.enable || cfg.proxy.enable) {
+      groups."bird-lg" = mkIf (cfg.group == "bird-lg") { };
+      users."bird-lg" = mkIf (cfg.user == "bird-lg") {
+        description = "Bird Looking Glass user";
+        extraGroups = lib.optionals (config.services.bird2.enable) [ "bird2" ];
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
index fc06cdaa6e58c..d409f06022897 100644
--- a/nixos/modules/services/networking/bird.nix
+++ b/nixos/modules/services/networking/bird.nix
@@ -3,103 +3,100 @@
 let
   inherit (lib) mkEnableOption mkIf mkOption optionalString types;
 
-  generic = variant:
-    let
-      cfg = config.services.${variant};
-      pkg = pkgs.${variant};
-      birdBin = if variant == "bird6" then "bird6" else "bird";
-      birdc = if variant == "bird6" then "birdc6" else "birdc";
-      descr =
-        { bird = "1.6.x with IPv4 support";
-          bird6 = "1.6.x with IPv6 support";
-          bird2 = "2.x";
-        }.${variant};
-    in {
-      ###### interface
-      options = {
-        services.${variant} = {
-          enable = mkEnableOption "BIRD Internet Routing Daemon (${descr})";
-          config = mkOption {
-            type = types.lines;
-            description = ''
-              BIRD Internet Routing Daemon configuration file.
-              <link xlink:href='http://bird.network.cz/'/>
-            '';
-          };
-          checkConfig = mkOption {
-            type = types.bool;
-            default = true;
-            description = ''
-              Whether the config should be checked at build time.
-              When the config can't be checked during build time, for example when it includes
-              other files, either disable this option or use <code>preCheckConfig</code> to create
-              the included files before checking.
-            '';
-          };
-          preCheckConfig = mkOption {
-            type = types.lines;
-            default = "";
-            example = ''
-              echo "cost 100;" > include.conf
-            '';
-            description = ''
-              Commands to execute before the config file check. The file to be checked will be
-              available as <code>${variant}.conf</code> in the current directory.
+  cfg = config.services.bird2;
+  caps = [ "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
+in
+{
+  ###### interface
+  options = {
+    services.bird2 = {
+      enable = mkEnableOption "BIRD Internet Routing Daemon";
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          BIRD Internet Routing Daemon configuration file.
+          <link xlink:href='http://bird.network.cz/'/>
+        '';
+      };
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the config should be checked at build time.
+          When the config can't be checked during build time, for example when it includes
+          other files, either disable this option or use <code>preCheckConfig</code> to create
+          the included files before checking.
+        '';
+      };
+      preCheckConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          echo "cost 100;" > include.conf
+        '';
+        description = ''
+          Commands to execute before the config file check. The file to be checked will be
+          available as <code>bird2.conf</code> in the current directory.
 
-              Files created with this option will not be available at service runtime, only during
-              build time checking.
-            '';
-          };
-        };
+          Files created with this option will not be available at service runtime, only during
+          build time checking.
+        '';
       };
+    };
+  };
 
-      ###### implementation
-      config = mkIf cfg.enable {
-        environment.systemPackages = [ pkg ];
 
-        environment.etc."bird/${variant}.conf".source = pkgs.writeTextFile {
-          name = "${variant}.conf";
-          text = cfg.config;
-          checkPhase = optionalString cfg.checkConfig ''
-            ln -s $out ${variant}.conf
-            ${cfg.preCheckConfig}
-            ${pkg}/bin/${birdBin} -d -p -c ${variant}.conf
-          '';
-        };
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "bird" ] "Use services.bird2 instead")
+    (lib.mkRemovedOptionModule [ "services" "bird6" ] "Use services.bird2 instead")
+  ];
 
-        systemd.services.${variant} = {
-          description = "BIRD Internet Routing Daemon (${descr})";
-          wantedBy = [ "multi-user.target" ];
-          reloadIfChanged = true;
-          restartTriggers = [ config.environment.etc."bird/${variant}.conf".source ];
-          serviceConfig = {
-            Type = "forking";
-            Restart = "on-failure";
-            ExecStart = "${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -u ${variant} -g ${variant}";
-            ExecReload = "/bin/sh -c '${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -p && ${pkg}/bin/${birdc} configure'";
-            ExecStop = "${pkg}/bin/${birdc} down";
-            CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_SETUID" "CAP_SETGID"
-                                      # see bird/sysdep/linux/syspriv.h
-                                      "CAP_NET_BIND_SERVICE" "CAP_NET_BROADCAST" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
-            ProtectSystem = "full";
-            ProtectHome = "yes";
-            SystemCallFilter="~@cpu-emulation @debug @keyring @module @mount @obsolete @raw-io";
-            MemoryDenyWriteExecute = "yes";
-          };
-        };
-        users = {
-          users.${variant} = {
-            description = "BIRD Internet Routing Daemon user";
-            group = variant;
-            isSystemUser = true;
-          };
-          groups.${variant} = {};
-        };
-      };
-    };
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.bird ];
 
-in
+    environment.etc."bird/bird2.conf".source = pkgs.writeTextFile {
+      name = "bird2";
+      text = cfg.config;
+      checkPhase = optionalString cfg.checkConfig ''
+        ln -s $out bird2.conf
+        ${cfg.preCheckConfig}
+        ${pkgs.bird}/bin/bird -d -p -c bird2.conf
+      '';
+    };
 
-{
-  imports = map generic [ "bird" "bird6" "bird2" ];
+    systemd.services.bird2 = {
+      description = "BIRD Internet Routing Daemon";
+      wantedBy = [ "multi-user.target" ];
+      reloadTriggers = [ config.environment.etc."bird/bird2.conf".source ];
+      serviceConfig = {
+        Type = "forking";
+        Restart = "on-failure";
+        User = "bird2";
+        Group = "bird2";
+        ExecStart = "${pkgs.bird}/bin/bird -c /etc/bird/bird2.conf";
+        ExecReload = "${pkgs.bird}/bin/birdc configure";
+        ExecStop = "${pkgs.bird}/bin/birdc down";
+        RuntimeDirectory = "bird";
+        CapabilityBoundingSet = caps;
+        AmbientCapabilities = caps;
+        ProtectSystem = "full";
+        ProtectHome = "yes";
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        SystemCallFilter = "~@cpu-emulation @debug @keyring @module @mount @obsolete @raw-io";
+        MemoryDenyWriteExecute = "yes";
+      };
+    };
+    users = {
+      users.bird2 = {
+        description = "BIRD Internet Routing Daemon user";
+        group = "bird2";
+        isSystemUser = true;
+      };
+      groups.bird2 = { };
+    };
+  };
 }
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 8bf04e3a1a23c..f76cffc79bfa4 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -174,6 +174,7 @@ in
         serviceConfig = {
           DynamicUser = true;
           StateDirectory = "bitlbee";
+          ReadWritePaths = [ cfg.configDir ];
           ExecStart = "${bitlbeePkg}/sbin/bitlbee -F -n -c ${bitlbeeConfig}";
         };
       };
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
new file mode 100644
index 0000000000000..7488e05fc0331
--- /dev/null
+++ b/nixos/modules/services/networking/blocky.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.blocky;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "config.yaml" cfg.settings;
+in
+{
+  options.services.blocky = {
+    enable = mkEnableOption "Fast and lightweight DNS proxy as ad-blocker for local network with many features";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Blocky configuration. Refer to
+        <link xlink:href="https://0xerr0r.github.io/blocky/configuration/"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.blocky = {
+      description = "A DNS proxy and ad-blocker for the local network";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.blocky}/bin/blocky --config ${configFile}";
+
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/cloudflare-dyndns.nix b/nixos/modules/services/networking/cloudflare-dyndns.nix
new file mode 100644
index 0000000000000..ab5b1a08539a5
--- /dev/null
+++ b/nixos/modules/services/networking/cloudflare-dyndns.nix
@@ -0,0 +1,93 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cloudflare-dyndns;
+in
+{
+  options = {
+    services.cloudflare-dyndns = {
+      enable = mkEnableOption "Cloudflare Dynamic DNS Client";
+
+      apiTokenFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The path to a file containing the CloudFlare API token.
+
+          The file must have the form `CLOUDFLARE_API_TOKEN=...`
+        '';
+      };
+
+      domains = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of domain names to update records for.
+        '';
+      };
+
+      proxied = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether this is a DNS-only record, or also being proxied through CloudFlare.
+        '';
+      };
+
+      ipv4 = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable setting IPv4 A records.
+        '';
+      };
+
+      ipv6 = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable setting IPv6 AAAA records.
+        '';
+      };
+
+      deleteMissing = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to delete the record when no IP address is found.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.cloudflare-dyndns = {
+      description = "CloudFlare Dynamic DNS Client";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      startAt = "*:0/5";
+
+      environment = {
+        CLOUDFLARE_DOMAINS = toString cfg.domains;
+      };
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "cloudflare-dyndns";
+        EnvironmentFile = cfg.apiTokenFile;
+        ExecStart =
+          let
+            args = [ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
+              ++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
+              ++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
+              ++ optional cfg.deleteMissing "--delete-missing"
+              ++ optional cfg.proxied "--proxied";
+          in
+          "${pkgs.cloudflare-dyndns}/bin/cloudflare-dyndns ${toString args}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index ca9c422e6d7cb..cb53cc01f52dd 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -80,13 +80,21 @@ in
             The name of the interface to pull the bind_addr from.
           '';
         };
+      };
 
+      forceAddrFamily = mkOption {
+        type = types.enum [ "any" "ipv4" "ipv6" ];
+        default = "any";
+        description = ''
+          Whether to bind ipv4/ipv6 or both kind of addresses.
+        '';
       };
 
       forceIpv4 = mkOption {
-        type = types.bool;
-        default = false;
+        type = types.nullOr types.bool;
+        default = null;
         description = ''
+          Deprecated: Use consul.forceAddrFamily instead.
           Whether we should force the interfaces to only pull ipv4 addresses.
         '';
       };
@@ -175,6 +183,13 @@ in
         systemPackages = [ cfg.package ];
       };
 
+      warnings = lib.flatten [
+        (lib.optional (cfg.forceIpv4 != null) ''
+          The option consul.forceIpv4 is deprecated, please use
+          consul.forceAddrFamily instead.
+        '')
+      ];
+
       systemd.services.consul = {
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ] ++ systemdDevices;
@@ -196,15 +211,21 @@ in
         });
 
         path = with pkgs; [ iproute2 gnugrep gawk consul ];
-        preStart = ''
+        preStart = let
+          family = if cfg.forceAddrFamily == "ipv6" then
+            "-6"
+          else if cfg.forceAddrFamily == "ipv4" then
+            "-4"
+          else
+            "";
+        in ''
           mkdir -m 0700 -p ${dataDir}
           chown -R consul ${dataDir}
 
           # Determine interface addresses
           getAddrOnce () {
-            ip addr show dev "$1" \
-              | grep 'inet${optionalString (cfg.forceIpv4) " "}.*scope global' \
-              | awk -F '[ /\t]*' '{print $3}' | head -n 1
+            ip ${family} addr show dev "$1" scope global \
+              | awk -F '[ /\t]*' '/inet/ {print $3}' | head -n 1
           }
           getAddr () {
             ADDR="$(getAddrOnce $1)"
@@ -234,6 +255,11 @@ in
       };
     }
 
+    # deprecated
+    (mkIf (cfg.forceIpv4 != null && cfg.forceIpv4) {
+      services.consul.forceAddrFamily = "ipv4";
+    })
+
     (mkIf (cfg.alerts.enable) {
       systemd.services.consul-alerts = {
         wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/networking/create_ap.nix b/nixos/modules/services/networking/create_ap.nix
new file mode 100644
index 0000000000000..a3c330fab0075
--- /dev/null
+++ b/nixos/modules/services/networking/create_ap.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.create_ap;
+  configFile = pkgs.writeText "create_ap.conf" (generators.toKeyValue { } cfg.settings);
+in {
+  options = {
+    services.create_ap = {
+      enable = mkEnableOption "setup wifi hotspots using create_ap";
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int bool str ]);
+        default = {};
+        description = ''
+          Configuration for <package>create_ap</package>.
+          See <link xlink:href="https://raw.githubusercontent.com/lakinduakash/linux-wifi-hotspot/master/src/scripts/create_ap.conf">upstream example configuration</link>
+          for supported values.
+        '';
+        example = {
+          INTERNET_IFACE = "eth0";
+          WIFI_IFACE = "wlan0";
+          SSID = "My Wifi Hotspot";
+          PASSPHRASE = "12345678";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      services.create_ap = {
+        wantedBy = [ "multi-user.target" ];
+        description = "Create AP Service";
+        after = [ "network.target" ];
+        restartTriggers = [ configFile ];
+        serviceConfig = {
+          ExecStart = "${pkgs.linux-wifi-hotspot}/bin/create_ap --config ${configFile}";
+          KillSignal = "SIGINT";
+          Restart = "on-failure";
+        };
+      };
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ onny ];
+
+}
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index d025c8f8177a8..43a50af8f9b59 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -13,7 +13,7 @@ let
     foreground=YES
     use=${cfg.use}
     login=${cfg.username}
-    password=${lib.optionalString (cfg.protocol == "nsupdate") "/run/${RuntimeDirectory}/ddclient.key"}
+    password=${if cfg.protocol == "nsupdate" then "/run/${RuntimeDirectory}/ddclient.key" else "@password_placeholder@"}
     protocol=${cfg.protocol}
     ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
     ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
@@ -33,10 +33,9 @@ let
     ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
       install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
     '' else if (cfg.passwordFile != null) then ''
-      password=$(printf "%q" "$(head -n 1 "${cfg.passwordFile}")")
-      sed -i "s|^password=$|password=$password|" /run/${RuntimeDirectory}/ddclient.conf
+      "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
     '' else ''
-      sed -i '/^password=$/d' /run/${RuntimeDirectory}/ddclient.conf
+      sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
     '')}
   '';
 
@@ -181,7 +180,7 @@ with lib;
       };
 
       verbose = mkOption {
-        default = true;
+        default = false;
         type = bool;
         description = ''
           Print verbose information.
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 3eb7ca99eafd4..a4c8608c31db8 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -215,7 +215,7 @@ in
         # dhcpcd.  So do a "systemctl restart" instead.
         stopIfChanged = false;
 
-        path = [ dhcpcd pkgs.nettools pkgs.openresolv ];
+        path = [ dhcpcd pkgs.nettools config.networking.resolvconf.package ];
 
         unitConfig.ConditionCapability = "CAP_NET_ADMIN";
 
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 3c4c0069dfd00..49950efc0a1bd 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -7,7 +7,7 @@ let
   cfg4 = config.services.dhcpd4;
   cfg6 = config.services.dhcpd6;
 
-  writeConfig = cfg: pkgs.writeText "dhcpd.conf"
+  writeConfig = postfix: cfg: pkgs.writeText "dhcpd.conf"
     ''
       default-lease-time 600;
       max-lease-time 7200;
@@ -21,7 +21,9 @@ let
           (machine: ''
             host ${machine.hostName} {
               hardware ethernet ${machine.ethernetAddress};
-              fixed-address ${machine.ipAddress};
+              fixed-address${
+                optionalString (postfix == "6") postfix
+              } ${machine.ipAddress};
             }
           '')
           cfg.machines
@@ -33,7 +35,7 @@ let
       configFile =
         if cfg.configFile != null
           then cfg.configFile
-          else writeConfig cfg;
+          else writeConfig postfix cfg;
       leaseFile = "/var/lib/dhcpd${postfix}/dhcpd.leases";
       args = [
         "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
diff --git a/nixos/modules/services/networking/envoy.nix b/nixos/modules/services/networking/envoy.nix
new file mode 100644
index 0000000000000..b7f859c73d9dd
--- /dev/null
+++ b/nixos/modules/services/networking/envoy.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.envoy;
+  format = pkgs.formats.json { };
+  conf = format.generate "envoy.json" cfg.settings;
+  validateConfig = file:
+    pkgs.runCommand "validate-envoy-conf" { } ''
+      ${pkgs.envoy}/bin/envoy --log-level error --mode validate -c "${file}"
+      cp "${file}" "$out"
+    '';
+
+in
+
+{
+  options.services.envoy = {
+    enable = mkEnableOption "Envoy reverse proxy";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      example = literalExpression ''
+        {
+          admin = {
+            access_log_path = "/dev/null";
+            address = {
+              socket_address = {
+                protocol = "TCP";
+                address = "127.0.0.1";
+                port_value = 9901;
+              };
+            };
+          };
+          static_resources = {
+            listeners = [];
+            clusters = [];
+          };
+        }
+      '';
+      description = ''
+        Specify the configuration for Envoy in Nix.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.envoy ];
+    systemd.services.envoy = {
+      description = "Envoy reverse proxy";
+      after = [ "network-online.target" ];
+      requires = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.envoy}/bin/envoy -c ${validateConfig conf}";
+        DynamicUser = true;
+        Restart = "no";
+        CacheDirectory = "envoy";
+        LogsDirectory = "envoy";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_XDP";
+        SystemCallArchitectures = "native";
+        LockPersonality = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
+        PrivateDevices = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "ptraceable";
+        ProtectHostname = true;
+        ProtectSystem = "strict";
+        UMask = "0066";
+        SystemCallFilter = "~@clock @module @mount @reboot @swap @obsolete @cpu-emulation";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/eternal-terminal.nix b/nixos/modules/services/networking/eternal-terminal.nix
index 88b4cd90540f4..0dcf3d28f4e0e 100644
--- a/nixos/modules/services/networking/eternal-terminal.nix
+++ b/nixos/modules/services/networking/eternal-terminal.nix
@@ -90,6 +90,6 @@ in
   };
 
   meta = {
-    maintainers = with lib.maintainers; [ pingiun ];
+    maintainers = with lib.maintainers; [ ];
   };
 }
diff --git a/nixos/modules/services/networking/expressvpn.nix b/nixos/modules/services/networking/expressvpn.nix
new file mode 100644
index 0000000000000..d8ae6528a4d4e
--- /dev/null
+++ b/nixos/modules/services/networking/expressvpn.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+{
+  options.services.expressvpn.enable = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      Enable the ExpressVPN daemon.
+    '';
+  };
+
+  config = mkIf config.services.expressvpn.enable {
+    boot.kernelModules = [ "tun" ];
+
+    systemd.services.expressvpn = {
+      description = "ExpressVPN Daemon";
+      serviceConfig = {
+        ExecStart = "${pkgs.expressvpn}/bin/expressvpnd";
+        Restart = "on-failure";
+        RestartSec = 5;
+      };
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ yureien ];
+}
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 2aa3be16f6e9b..c213a5516a498 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -179,10 +179,6 @@ let
       ) cfg.allowedUDPPortRanges
     ) allInterfaces)}
 
-    # Accept IPv4 multicast.  Not a big security risk since
-    # probably nobody is listening anyway.
-    #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept
-
     # Optionally respond to ICMPv4 pings.
     ${optionalString cfg.allowPing ''
       iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
@@ -437,8 +433,6 @@ in
             drop the packet if the source address is not reachable via any
             interface) or false.  Defaults to the value of
             kernelHasRPFilter.
-
-            (needs kernel 3.3+)
           '';
       };
 
diff --git a/nixos/modules/services/networking/flannel.nix b/nixos/modules/services/networking/flannel.nix
index ac84b3d35a3d8..4698c1f682c99 100644
--- a/nixos/modules/services/networking/flannel.nix
+++ b/nixos/modules/services/networking/flannel.nix
@@ -155,10 +155,11 @@ in {
         FLANNELD_ETCD_KEYFILE = cfg.etcd.keyFile;
         FLANNELD_ETCD_CERTFILE = cfg.etcd.certFile;
         FLANNELD_ETCD_CAFILE = cfg.etcd.caFile;
-        ETCDCTL_CERT_FILE = cfg.etcd.certFile;
-        ETCDCTL_KEY_FILE = cfg.etcd.keyFile;
-        ETCDCTL_CA_FILE = cfg.etcd.caFile;
-        ETCDCTL_PEERS = concatStringsSep "," cfg.etcd.endpoints;
+        ETCDCTL_CERT = cfg.etcd.certFile;
+        ETCDCTL_KEY = cfg.etcd.keyFile;
+        ETCDCTL_CACERT = cfg.etcd.caFile;
+        ETCDCTL_ENDPOINTS = concatStringsSep "," cfg.etcd.endpoints;
+        ETCDCTL_API = "3";
       } // optionalAttrs (cfg.storageBackend == "kubernetes") {
         FLANNELD_KUBE_SUBNET_MGR = "true";
         FLANNELD_KUBECONFIG_FILE = cfg.kubeconfig;
@@ -167,7 +168,7 @@ in {
       path = [ pkgs.iptables ];
       preStart = optionalString (cfg.storageBackend == "etcd") ''
         echo "setting network configuration"
-        until ${pkgs.etcd}/bin/etcdctl set /coreos.com/network/config '${builtins.toJSON networkConfig}'
+        until ${pkgs.etcd}/bin/etcdctl put /coreos.com/network/config '${builtins.toJSON networkConfig}'
         do
           echo "setting network configuration, retry"
           sleep 1
diff --git a/nixos/modules/services/networking/frr.nix b/nixos/modules/services/networking/frr.nix
index 45a82b9450a44..98452123f0306 100644
--- a/nixos/modules/services/networking/frr.nix
+++ b/nixos/modules/services/networking/frr.nix
@@ -106,6 +106,14 @@ let
           TCP Port to bind to for the VTY interface.
         '';
       };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra options for the daemon.
+        '';
+      };
     };
 
 in
@@ -196,7 +204,8 @@ in
                 PIDFile = "frr/${daemon}.pid";
                 ExecStart = "${pkgs.frr}/libexec/frr/${daemon} -f /etc/frr/${service}.conf"
                   + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
-                  + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}";
+                  + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}"
+                  + " " + (concatStringsSep " " scfg.extraOptions);
                 ExecReload = "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemonName service} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${service}.conf";
                 Restart = "on-abnormal";
               };
diff --git a/nixos/modules/services/networking/gateone.nix b/nixos/modules/services/networking/gateone.nix
index 3e3a3c1aa94d4..e68f8a47d5c0d 100644
--- a/nixos/modules/services/networking/gateone.nix
+++ b/nixos/modules/services/networking/gateone.nix
@@ -36,11 +36,11 @@ config = mkIf cfg.enable {
     preStart = ''
       if [ ! -d ${cfg.settingsDir} ] ; then
         mkdir -m 0750 -p ${cfg.settingsDir}
-        chown -R gateone.gateone ${cfg.settingsDir}
+        chown -R gateone:gateone ${cfg.settingsDir}
       fi
       if [ ! -d ${cfg.pidDir} ] ; then
         mkdir -m 0750 -p ${cfg.pidDir}
-        chown -R gateone.gateone ${cfg.pidDir}
+        chown -R gateone:gateone ${cfg.pidDir}
       fi
       '';
     #unitConfig.RequiresMountsFor = "${cfg.settingsDir}";
diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix
index 091d2a938cd4e..5b07beadb45f8 100644
--- a/nixos/modules/services/networking/headscale.nix
+++ b/nixos/modules/services/networking/headscale.nix
@@ -479,7 +479,7 @@ in
           NoNewPrivileges = true;
           LockPersonality = true;
           RestrictRealtime = true;
-          SystemCallFilter = [ "@system-service" "~@priviledged" "@chown" ];
+          SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
           SystemCallArchitectures = "native";
           RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
         };
diff --git a/nixos/modules/services/networking/https-dns-proxy.nix b/nixos/modules/services/networking/https-dns-proxy.nix
new file mode 100644
index 0000000000000..85d6c362b466b
--- /dev/null
+++ b/nixos/modules/services/networking/https-dns-proxy.nix
@@ -0,0 +1,128 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    concatStringsSep
+    mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.https-dns-proxy;
+
+  providers = {
+    cloudflare = {
+      ips = [ "1.1.1.1" "1.0.0.1" ];
+      url = "https://cloudflare-dns.com/dns-query";
+    };
+    google = {
+      ips = [ "8.8.8.8" "8.8.4.4" ];
+      url = "https://dns.google/dns-query";
+    };
+    quad9 = {
+      ips = [ "9.9.9.9" "149.112.112.112" ];
+      url = "https://dns.quad9.net/dns-query";
+    };
+  };
+
+  defaultProvider = "quad9";
+
+  providerCfg =
+    let
+      isCustom = cfg.provider.kind == "custom";
+    in
+    lib.concatStringsSep " " [
+      "-b"
+      (concatStringsSep "," (if isCustom then cfg.provider.ips else providers."${cfg.provider.kind}".ips))
+      "-r"
+      (if isCustom then cfg.provider.url else providers."${cfg.provider.kind}".url)
+    ];
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  ###### interface
+
+  options.services.https-dns-proxy = {
+    enable = mkEnableOption "https-dns-proxy daemon";
+
+    address = mkOption {
+      description = "The address on which to listen";
+      type = types.str;
+      default = "127.0.0.1";
+    };
+
+    port = mkOption {
+      description = "The port on which to listen";
+      type = types.port;
+      default = 5053;
+    };
+
+    provider = {
+      kind = mkOption {
+        description = ''
+          The upstream provider to use or custom in case you do not trust any of
+          the predefined providers or just want to use your own.
+
+          The default is ${defaultProvider} and there are privacy and security trade-offs
+          when using any upstream provider. Please consider that before using any
+          of them.
+
+          If you pick a custom provider, you will need to provide the bootstrap
+          IP addresses as well as the resolver https URL.
+        '';
+        type = types.enum ((builtins.attrNames providers) ++ [ "custom" ]);
+        default = defaultProvider;
+      };
+
+      ips = mkOption {
+        description = "The custom provider IPs";
+        type = types.listOf types.str;
+      };
+
+      url = mkOption {
+        description = "The custom provider URL";
+        type = types.str;
+      };
+    };
+
+    preferIPv4 = mkOption {
+      description = ''
+        https_dns_proxy will by default use IPv6 and fail if it is not available.
+        To play it safe, we choose IPv4.
+      '';
+      type = types.bool;
+      default = true;
+    };
+
+    extraArgs = mkOption {
+      description = "Additional arguments to pass to the process.";
+      type = types.listOf types.str;
+      default = [ "-v" ];
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.https-dns-proxy = {
+      description = "DNS to DNS over HTTPS (DoH) proxy";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = rec {
+        Type = "exec";
+        DynamicUser = true;
+        ExecStart = lib.concatStringsSep " " (
+          [
+            "${pkgs.https-dns-proxy}/bin/https_dns_proxy"
+            "-a ${toString cfg.address}"
+            "-p ${toString cfg.port}"
+            "-l -"
+            providerCfg
+          ]
+          ++ lib.optional cfg.preferIPv4 "-4"
+          ++ cfg.extraArgs
+        );
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
index 8e59c68054d2a..8f621b61002fc 100644
--- a/nixos/modules/services/networking/hylafax/options.nix
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib.options) literalExpression mkEnableOption mkOption;
-  inherit (lib.types) bool enum ints lines attrsOf nullOr path str submodule;
+  inherit (lib.types) bool enum ints lines attrsOf nonEmptyStr nullOr path str submodule;
   inherit (lib.modules) mkDefault mkIf mkMerge;
 
   commonDescr = ''
@@ -17,8 +17,6 @@ let
     configuration to yield an operational system.
   '';
 
-  str1 = lib.types.addCheck str (s: s!="");  # non-empty string
-
   configAttrType =
     # Options in HylaFAX configuration files can be
     # booleans, strings, integers, or list thereof
@@ -37,7 +35,7 @@ let
   modemConfigOptions = { name, config, ... }: {
     options = {
       name = mkOption {
-        type = str1;
+        type = nonEmptyStr;
         example = "ttyS1";
         description = ''
           Name of modem device,
@@ -45,7 +43,7 @@ let
         '';
       };
       type = mkOption {
-        type = str1;
+        type = nonEmptyStr;
         example = "cirrus";
         description = ''
           Name of modem configuration file,
@@ -135,14 +133,14 @@ in
     };
 
     countryCode = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "49";
       description = "Country code for server and all modems.";
     };
 
     areaCode = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "30";
       description = "Area code for server and all modems.";
@@ -279,7 +277,7 @@ in
       each time the spooling area is initialized.
     '';
     faxcron.enable.frequency = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "daily";
       description = ''
@@ -319,7 +317,7 @@ in
       each time the spooling area is initialized.
     '';
     faxqclean.enable.frequency = mkOption {
-      type = nullOr str1;
+      type = nullOr nonEmptyStr;
       default = null;
       example = "daily";
       description = ''
diff --git a/nixos/modules/services/networking/i2pd.nix b/nixos/modules/services/networking/i2pd.nix
index 34fda57b23d23..06f3420b8ff83 100644
--- a/nixos/modules/services/networking/i2pd.nix
+++ b/nixos/modules/services/networking/i2pd.nix
@@ -158,6 +158,10 @@ let
       (sec "addressbook")
       (strOpt "defaulturl" cfg.addressbook.defaulturl)
     ] ++ (optionalEmptyList "subscriptions" cfg.addressbook.subscriptions)
+      ++ [
+      (sec "meshnets")
+      (boolOpt "yggdrasil" cfg.yggdrasil.enable)
+    ] ++ (optionalNullString "yggaddress" cfg.yggdrasil.address)
       ++ (flip map
       (collect (proto: proto ? port && proto ? address) cfg.proto)
       (proto: let protoOpts = [
@@ -546,6 +550,17 @@ in
         '';
       };
 
+      yggdrasil.enable = mkEnableOption "Yggdrasil";
+
+      yggdrasil.address = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Your local yggdrasil address. Specify it if you want to bind your router to a
+          particular address.
+        '';
+      };
+
       proto.http = (mkEndpointOpt "http" "127.0.0.1" 7070) // {
 
         auth = mkEnableOption "Webconsole authentication";
diff --git a/nixos/modules/services/networking/ircd-hybrid/ircd.conf b/nixos/modules/services/networking/ircd-hybrid/ircd.conf
index 17ef203840af5..b82094cf5f093 100644
--- a/nixos/modules/services/networking/ircd-hybrid/ircd.conf
+++ b/nixos/modules/services/networking/ircd-hybrid/ircd.conf
@@ -98,7 +98,7 @@ serverinfo {
 	 * 
 	 * 	openssl genrsa -out rsa.key 2048
 	 *	openssl rsa -in rsa.key -pubout -out rsa.pub
-	 *	chown <ircd-user>.<ircd.group> rsa.key rsa.pub
+	 *	chown <ircd-user>:<ircd.group> rsa.key rsa.pub
 	 *	chmod 0600 rsa.key
 	 *	chmod 0644 rsa.pub
 	 */
diff --git a/nixos/modules/services/networking/iwd.nix b/nixos/modules/services/networking/iwd.nix
index 8835f7f9372db..5c1480e7e2fb9 100644
--- a/nixos/modules/services/networking/iwd.nix
+++ b/nixos/modules/services/networking/iwd.nix
@@ -1,12 +1,21 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
+  inherit (lib)
+    mkEnableOption mkIf mkOption types
+    recursiveUpdate;
+
   cfg = config.networking.wireless.iwd;
   ini = pkgs.formats.ini { };
-  configFile = ini.generate "main.conf" cfg.settings;
-in {
+  defaults = {
+    # without UseDefaultInterface, sometimes wlan0 simply goes AWOL with NetworkManager
+    # https://iwd.wiki.kernel.org/interface_lifecycle#interface_management_in_iwd
+    General.UseDefaultInterface = with config.networking.networkmanager; (enable && (wifi.backend == "iwd"));
+  };
+  configFile = ini.generate "main.conf" (recursiveUpdate defaults cfg.settings);
+
+in
+{
   options.networking.wireless.iwd = {
     enable = mkEnableOption "iwd";
 
@@ -38,10 +47,10 @@ in {
       '';
     }];
 
-    environment.etc."iwd/main.conf".source = configFile;
+    environment.etc."iwd/${configFile.name}".source = configFile;
 
     # for iwctl
-    environment.systemPackages =  [ pkgs.iwd ];
+    environment.systemPackages = [ pkgs.iwd ];
 
     services.dbus.packages = [ pkgs.iwd ];
 
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index 17b4eb2e283be..994c511bdc2d6 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -9,20 +9,26 @@ with lib;
 let
   cfg = config.services.kea;
 
+  xor = x: y: (!x && y) || (x && !y);
   format = pkgs.formats.json {};
 
-  ctrlAgentConfig = format.generate "kea-ctrl-agent.conf" {
+  chooseNotNull = x: y: if x != null then x else y;
+
+  ctrlAgentConfig = chooseNotNull cfg.ctrl-agent.configFile (format.generate "kea-ctrl-agent.conf" {
     Control-agent = cfg.ctrl-agent.settings;
-  };
-  dhcp4Config = format.generate "kea-dhcp4.conf" {
+  });
+
+  dhcp4Config = chooseNotNull cfg.dhcp4.configFile (format.generate "kea-dhcp4.conf" {
     Dhcp4 = cfg.dhcp4.settings;
-  };
-  dhcp6Config = format.generate "kea-dhcp6.conf" {
+  });
+
+  dhcp6Config = chooseNotNull cfg.dhcp6.configFile (format.generate "kea-dhcp6.conf" {
     Dhcp6 = cfg.dhcp6.settings;
-  };
-  dhcpDdnsConfig = format.generate "kea-dhcp-ddns.conf" {
+  });
+
+  dhcpDdnsConfig = chooseNotNull cfg.dhcp-ddns.configFile (format.generate "kea-dhcp-ddns.conf" {
     DhcpDdns = cfg.dhcp-ddns.settings;
-  };
+  });
 
   package = pkgs.kea;
 in
@@ -45,6 +51,17 @@ in
             '';
           };
 
+          configFile = mkOption {
+            type = nullOr path;
+            default = null;
+            description = ''
+              Kea Control Agent configuration as a path, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"/>.
+
+              Takes preference over <link linkend="opt-services.kea.ctrl-agent.settings">settings</link>.
+              Most users should prefer using <link linkend="opt-services.kea.ctrl-agent.settings">settings</link> instead.
+            '';
+          };
+
           settings = mkOption {
             type = format.type;
             default = null;
@@ -73,6 +90,17 @@ in
             '';
           };
 
+          configFile = mkOption {
+            type = nullOr path;
+            default = null;
+            description = ''
+              Kea DHCP4 configuration as a path, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html"/>.
+
+              Takes preference over <link linkend="opt-services.kea.dhcp4.settings">settings</link>.
+              Most users should prefer using <link linkend="opt-services.kea.dhcp4.settings">settings</link> instead.
+            '';
+          };
+
           settings = mkOption {
             type = format.type;
             default = null;
@@ -122,6 +150,17 @@ in
             '';
           };
 
+          configFile = mkOption {
+            type = nullOr path;
+            default = null;
+            description = ''
+              Kea DHCP6 configuration as a path, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html"/>.
+
+              Takes preference over <link linkend="opt-services.kea.dhcp6.settings">settings</link>.
+              Most users should prefer using <link linkend="opt-services.kea.dhcp6.settings">settings</link> instead.
+            '';
+          };
+
           settings = mkOption {
             type = format.type;
             default = null;
@@ -172,6 +211,17 @@ in
             '';
           };
 
+          configFile = mkOption {
+            type = nullOr path;
+            default = null;
+            description = ''
+              Kea DHCP-DDNS configuration as a path, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"/>.
+
+              Takes preference over <link linkend="opt-services.kea.dhcp-ddns.settings">settings</link>.
+              Most users should prefer using <link linkend="opt-services.kea.dhcp-ddns.settings">settings</link> instead.
+            '';
+          };
+
           settings = mkOption {
             type = format.type;
             default = null;
@@ -214,6 +264,10 @@ in
   }
 
   (mkIf cfg.ctrl-agent.enable {
+    assertions = [{
+        assertion = xor (cfg.ctrl-agent.settings == null) (cfg.ctrl-agent.configFile == null);
+        message = "Either services.kea.ctrl-agent.settings or services.kea.ctrl-agent.configFile must be set to a non-null value.";
+    }];
 
     environment.etc."kea/ctrl-agent.conf".source = ctrlAgentConfig;
 
@@ -252,6 +306,10 @@ in
   })
 
   (mkIf cfg.dhcp4.enable {
+    assertions = [{
+        assertion = xor (cfg.dhcp4.settings == null) (cfg.dhcp4.configFile == null);
+        message = "Either services.kea.dhcp4.settings or services.kea.dhcp4.configFile must be set to a non-null value.";
+    }];
 
     environment.etc."kea/dhcp4-server.conf".source = dhcp4Config;
 
@@ -295,6 +353,10 @@ in
   })
 
   (mkIf cfg.dhcp6.enable {
+    assertions = [{
+        assertion = xor (cfg.dhcp6.settings == null) (cfg.dhcp6.configFile == null);
+        message = "Either services.kea.dhcp6.settings or services.kea.dhcp6.configFile must be set to a non-null value.";
+    }];
 
     environment.etc."kea/dhcp6-server.conf".source = dhcp6Config;
 
@@ -336,6 +398,10 @@ in
   })
 
   (mkIf cfg.dhcp-ddns.enable {
+    assertions = [{
+        assertion = xor (cfg.dhcp-ddns.settings == null) (cfg.dhcp-ddns.configFile == null);
+        message = "Either services.kea.dhcp-ddns.settings or services.kea.dhcp-ddns.configFile must be set to a non-null value.";
+    }];
 
     environment.etc."kea/dhcp-ddns.conf".source = dhcpDdnsConfig;
 
diff --git a/nixos/modules/services/networking/lokinet.nix b/nixos/modules/services/networking/lokinet.nix
new file mode 100644
index 0000000000000..cf091341c8390
--- /dev/null
+++ b/nixos/modules/services/networking/lokinet.nix
@@ -0,0 +1,157 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.lokinet;
+  dataDir = "/var/lib/lokinet";
+  settingsFormat = pkgs.formats.ini { listsAsDuplicateKeys = true; };
+  configFile = settingsFormat.generate "lokinet.ini" (lib.filterAttrsRecursive (n: v: v != null) cfg.settings);
+in with lib; {
+  options.services.lokinet = {
+    enable = mkEnableOption "Lokinet daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.lokinet;
+      defaultText = literalExpression "pkgs.lokinet";
+      description = "Lokinet package to use.";
+    };
+
+    useLocally = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = "Whether to use Lokinet locally.";
+    };
+
+    settings = mkOption {
+      type = with types;
+        submodule {
+          freeformType = settingsFormat.type;
+
+          options = {
+            dns = {
+              bind = mkOption {
+                type = str;
+                default = "127.3.2.1";
+                description = "Address to bind to for handling DNS requests.";
+              };
+
+              upstream = mkOption {
+                type = listOf str;
+                default = [ "9.9.9.10" ];
+                example = [ "1.1.1.1" "8.8.8.8" ];
+                description = ''
+                  Upstream resolver(s) to use as fallback for non-loki addresses.
+                  Multiple values accepted.
+                '';
+              };
+            };
+
+            network = {
+              exit = mkOption {
+                type = bool;
+                default = false;
+                description = ''
+                  Whether to act as an exit node. Beware that this
+                  increases demand on the server and may pose liability concerns.
+                  Enable at your own risk.
+                '';
+              };
+
+              exit-node = mkOption {
+                type = nullOr (listOf str);
+                default = null;
+                example = ''
+                  exit-node = [ "example.loki" ];              # maps all exit traffic to example.loki
+                  exit-node = [ "example.loki:100.0.0.0/24" ]; # maps 100.0.0.0/24 to example.loki
+                '';
+                description = ''
+                  Specify a `.loki` address and an optional ip range to use as an exit broker.
+                  See <link xlink:href="http://probably.loki/wiki/index.php?title=Exit_Nodes"/> for
+                  a list of exit nodes.
+                '';
+              };
+
+              keyfile = mkOption {
+                type = nullOr str;
+                default = null;
+                example = "snappkey.private";
+                description = ''
+                  The private key to persist address with. If not specified the address will be ephemeral.
+                  This keyfile is generated automatically if the specified file doesn't exist.
+                '';
+              };
+            };
+          };
+        };
+      default = { };
+      example = literalExpression ''
+        {
+          dns = {
+            bind = "127.3.2.1";
+            upstream = [ "1.1.1.1" "8.8.8.8" ];
+          };
+
+          network.exit-node = [ "example.loki" "example2.loki" ];
+        }
+      '';
+      description = ''
+        Configuration for Lokinet.
+        Currently, the best way to view the available settings is by
+        generating a config file using `lokinet -g`.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.resolvconf.extraConfig = mkIf cfg.useLocally ''
+      name_servers="${cfg.settings.dns.bind}"
+    '';
+
+    systemd.services.lokinet = {
+      description = "Lokinet";
+      after = [ "network-online.target" "network.target" ];
+      wants = [ "network-online.target" "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        ln -sf ${cfg.package}/share/bootstrap.signed ${dataDir}
+        ${pkgs.coreutils}/bin/install -m 600 ${configFile} ${dataDir}/lokinet.ini
+
+        ${optionalString (cfg.settings.network.keyfile != null) ''
+          ${pkgs.crudini}/bin/crudini --set ${dataDir}/lokinet.ini network keyfile "${dataDir}/${cfg.settings.network.keyfile}"
+        ''}
+      '';
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "lokinet";
+        AmbientCapabilities = [ "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" ];
+        ExecStart = "${cfg.package}/bin/lokinet ${dataDir}/lokinet.ini";
+        Restart = "always";
+        RestartSec = "5s";
+
+        # hardening
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateMounts = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        ReadWritePaths = "/dev/net/tun";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/networking/lxd-image-server.nix b/nixos/modules/services/networking/lxd-image-server.nix
index b119ba8acf634..d326626eed442 100644
--- a/nixos/modules/services/networking/lxd-image-server.nix
+++ b/nixos/modules/services/networking/lxd-image-server.nix
@@ -51,18 +51,14 @@ in
 
       environment.etc."lxd-image-server/config.toml".source = format.generate "config.toml" cfg.settings;
 
-      services.logrotate.paths.lxd-image-server = {
-        path = "/var/log/lxd-image-server/lxd-image-server.log";
+      services.logrotate.settings.lxd-image-server = {
+        files = "/var/log/lxd-image-server/lxd-image-server.log";
         frequency = "daily";
-        keep = 21;
-        extraConfig = ''
-          create 755 lxd-image-server ${cfg.group}
-          missingok
-          compress
-          delaycompress
-          copytruncate
-          notifempty
-        '';
+        rotate = 21;
+        create = "755 lxd-image-server ${cfg.group}";
+        compress = true;
+        delaycompress = true;
+        copytruncate = true;
       };
 
       systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix
index c860f63efa673..57c37718e7cb5 100644
--- a/nixos/modules/services/networking/minidlna.nix
+++ b/nixos/modules/services/networking/minidlna.nix
@@ -5,164 +5,136 @@ with lib;
 
 let
   cfg = config.services.minidlna;
-  port = 8200;
+  settingsFormat = pkgs.formats.keyValue { listsAsDuplicateKeys = true; };
+  settingsFile = settingsFormat.generate "minidlna.conf" cfg.settings;
 in
 
 {
   ###### interface
-  options = {
-    services.minidlna.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description =
-        ''
-          Whether to enable MiniDLNA, a simple DLNA server.  It serves
-          media files such as video and music to DLNA client devices
-          such as televisions and media players.
-        '';
-    };
+  options.services.minidlna.enable = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      Whether to enable MiniDLNA, a simple DLNA server.
+      It serves media files such as video and music to DLNA client devices
+      such as televisions and media players. If you use the firewall consider
+      adding the following: <literal>services.minidlna.openFirewall = true;</literal>
+    '';
+  };
+
+  options.services.minidlna.openFirewall = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      Whether to open both HTTP (TCP) and SSDP (UDP) ports in the firewall.
+    '';
+  };
 
-    services.minidlna.mediaDirs = mkOption {
-      type = types.listOf types.str;
-      default = [];
-      example = [ "/data/media" "V,/home/alice/video" ];
-      description =
-        ''
-          Directories to be scanned for media files.  The prefixes
-          <literal>A,</literal>, <literal>V,</literal> and
+  options.services.minidlna.settings = mkOption {
+    default = {};
+    description = ''
+      The contents of MiniDLNA's configuration file.
+      When the service is activated, a basic template is generated
+      from the current options opened here.
+    '';
+    type = types.submodule {
+      freeformType = settingsFormat.type;
+
+      options.media_dir = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "/data/media" "V,/home/alice/video" ];
+        description = ''
+          Directories to be scanned for media files.
+          The prefixes <literal>A,</literal>,<literal>V,</literal> and
           <literal>P,</literal> restrict a directory to audio, video
-          or image files.  The directories must be accessible to the
+          or image files. The directories must be accessible to the
           <literal>minidlna</literal> user account.
         '';
-    };
-
-    services.minidlna.friendlyName = mkOption {
-      type = types.str;
-      default = "${config.networking.hostName} MiniDLNA";
-      defaultText = literalExpression ''"''${config.networking.hostName} MiniDLNA"'';
-      example = "rpi3";
-      description =
-        ''
-          Name that the DLNA server presents to clients.
-        '';
-    };
-
-    services.minidlna.rootContainer = mkOption {
-      type = types.str;
-      default = ".";
-      example = "B";
-      description =
-        ''
-          Use a different container as the root of the directory tree presented
-          to clients. The possible values are:
-          - "." - standard container
-          - "B" - "Browse Directory"
-          - "M" - "Music"
-          - "P" - "Pictures"
-          - "V" - "Video"
-          - Or, you can specify the ObjectID of your desired root container
-            (eg. 1$F for Music/Playlists)
-          If you specify "B" and the client device is audio-only then
-          "Music/Folders" will be used as root.
-         '';
-    };
-
-    services.minidlna.loglevel = mkOption {
-      type = types.str;
-      default = "warn";
-      example = "general,artwork,database,inotify,scanner,metadata,http,ssdp,tivo=warn";
-      description =
-        ''
-          Defines the type of messages that should be logged, and down to
-          which level of importance they should be considered.
-
-          The possible types are “artwork”, “database”, “general”, “http”,
-          “inotify”, “metadata”, “scanner”, “ssdp” and “tivo”.
-
-          The levels are “off”, “fatal”, “error”, “warn”, “info” and
-          “debug”, listed here in order of decreasing importance.  “off”
-          turns off logging messages entirely, “fatal” logs the most
-          critical messages only, and so on down to “debug” that logs every
-          single messages.
-
-          The types are comma-separated, followed by an equal sign (‘=’),
-          followed by a level that applies to the preceding types. This can
-          be repeated, separating each of these constructs with a comma.
-
-          Defaults to “general,artwork,database,inotify,scanner,metadata,
-          http,ssdp,tivo=warn” which logs every type of message at the
-          “warn” level.
-        '';
-    };
-
-    services.minidlna.announceInterval = mkOption {
-      type = types.int;
-      default = 895;
-      description =
-        ''
+      };
+      options.notify_interval = mkOption {
+        type = types.int;
+        default = 90000;
+        description = ''
           The interval between announces (in seconds).
-
-          By default miniDLNA will announce its presence on the network
-          approximately every 15 minutes.
-
-          Many people prefer shorter announce intervals (e.g. 60 seconds)
-          on their home networks, especially when DLNA clients are
-          started on demand.
+          Instead of waiting on announces, one can open port UDP 1900 or
+          set <literal>openFirewall</literal> option to use SSDP discovery.
+          Furthermore announce interval has now been set as 90000 in order
+          to prevent disconnects with certain clients and to rely solely
+          on the SSDP method.
+
+          Lower values (e.g. 60 seconds) should be used if one does not
+          want to utilize SSDP. By default miniDLNA will announce its
+          presence on the network approximately every 15 minutes. Many
+          people prefer shorter announce intervals on their home networks,
+          especially when DLNA clients are started on demand.
+
+          Some relevant information can be found here:
+          https://sourceforge.net/p/minidlna/discussion/879957/thread/1389d197/
         '';
-    };
-
-    services.minidlna.config = mkOption {
-      type = types.lines;
-      description =
-      ''
-        The contents of MiniDLNA's configuration file.
-        When the service is activated, a basic template is generated
-        from the current options opened here.
-      '';
-    };
-
-    services.minidlna.extraConfig = mkOption {
-      type = types.lines;
-      default = "";
-      example = ''
-        # Not exhaustive example
-        # Support for streaming .jpg and .mp3 files to a TiVo supporting HMO.
-        enable_tivo=no
-        # SSDP notify interval, in seconds.
-        notify_interval=10
-        # maximum number of simultaneous connections
-        # note: many clients open several simultaneous connections while
-        # streaming
-        max_connections=50
-        # set this to yes to allow symlinks that point outside user-defined
-        # media_dirs.
-        wide_links=yes
-      '';
-      description =
-      ''
-        Extra minidlna options not yet opened for configuration here
-        (strict_dlna, model_number, model_name, etc...).  This is appended
-        to the current service already provided.
-      '';
+      };
+      options.port = mkOption {
+        type = types.port;
+        default = 8200;
+        description = "Port number for HTTP traffic (descriptions, SOAP, media transfer).";
+      };
+      options.db_dir = mkOption {
+        type = types.path;
+        default = "/var/cache/minidlna";
+        example = "/tmp/minidlna";
+        description = "Specify the directory where you want MiniDLNA to store its database and album art cache.";
+      };
+      options.friendly_name = mkOption {
+        type = types.str;
+        default = "${config.networking.hostName} MiniDLNA";
+        defaultText = literalExpression "config.networking.hostName";
+        example = "rpi3";
+        description = "Name that the DLNA server presents to clients.";
+      };
+      options.root_container = mkOption {
+        type = types.str;
+        default = ".";
+        example = "B";
+        description = "Use a different container as the root of the directory tree presented to clients.";
+      };
+      options.log_level = mkOption {
+        type = types.str;
+        default = "warn";
+        example = "general,artwork,database,inotify,scanner,metadata,http,ssdp,tivo=warn";
+        description = "Defines the type of messages that should be logged and down to which level of importance.";
+      };
+      options.inotify = mkOption {
+        type = types.enum [ "yes" "no" ];
+        default = "no";
+        description = "Whether to enable inotify monitoring to automatically discover new files.";
+      };
+      options.enable_tivo = mkOption {
+        type = types.enum [ "yes" "no" ];
+        default = "no";
+        description = "Support for streaming .jpg and .mp3 files to a TiVo supporting HMO.";
+      };
+      options.wide_links = mkOption {
+        type = types.enum [ "yes" "no" ];
+        default = "no";
+        description = "Set this to yes to allow symlinks that point outside user-defined media_dirs.";
+      };
     };
   };
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "minidlna" "config" ] "")
+    (mkRemovedOptionModule [ "services" "minidlna" "extraConfig" ] "")
+    (mkRenamedOptionModule [ "services" "minidlna" "loglevel"] [ "services" "minidlna" "settings" "log_level" ])
+    (mkRenamedOptionModule [ "services" "minidlna" "rootContainer"] [ "services" "minidlna" "settings" "root_container" ])
+    (mkRenamedOptionModule [ "services" "minidlna" "mediaDirs"] [ "services" "minidlna" "settings" "media_dir" ])
+    (mkRenamedOptionModule [ "services" "minidlna" "friendlyName"] [ "services" "minidlna" "settings" "friendly_name" ])
+    (mkRenamedOptionModule [ "services" "minidlna" "announceInterval"] [ "services" "minidlna" "settings" "notify_interval" ])
+  ];
+
   ###### implementation
   config = mkIf cfg.enable {
-    services.minidlna.config =
-      ''
-        port=${toString port}
-        friendly_name=${cfg.friendlyName}
-        db_dir=/var/cache/minidlna
-        log_level=${cfg.loglevel}
-        inotify=yes
-        root_container=${cfg.rootContainer}
-        ${concatMapStrings (dir: ''
-          media_dir=${dir}
-        '') cfg.mediaDirs}
-        notify_interval=${toString cfg.announceInterval}
-        ${cfg.extraConfig}
-      '';
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.port ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ 1900 ];
 
     users.users.minidlna = {
       description = "MiniDLNA daemon user";
@@ -186,7 +158,7 @@ in
             PIDFile = "/run/minidlna/pid";
             ExecStart =
               "${pkgs.minidlna}/sbin/minidlnad -S -P /run/minidlna/pid" +
-              " -f ${pkgs.writeText "minidlna.conf" cfg.config}";
+              " -f ${settingsFile}";
           };
       };
   };
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index b41a2fd27be2f..70c6725d1035c 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -54,10 +54,10 @@ let
       hashedPassword = mkOption {
         type = uniq (nullOr str);
         default = null;
-        description = ''
+        description = mdDoc ''
           Specifies the hashed password for the MQTT User.
-          To generate hashed password install <literal>mosquitto</literal>
-          package and use <literal>mosquitto_passwd</literal>.
+          To generate hashed password install `mosquitto`
+          package and use `mosquitto_passwd`.
         '';
       };
 
@@ -65,11 +65,11 @@ let
         type = uniq (nullOr types.path);
         example = "/path/to/file";
         default = null;
-        description = ''
+        description = mdDoc ''
           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>.
+          To generate hashed password install `mosquitto`
+          package and use `mosquitto_passwd`.
         '';
       };
 
@@ -155,24 +155,24 @@ let
     options = {
       plugin = mkOption {
         type = path;
-        description = ''
-          Plugin path to load, should be a <literal>.so</literal> file.
+        description = mdDoc ''
+          Plugin path to load, should be a `.so` file.
         '';
       };
 
       denySpecialChars = mkOption {
         type = bool;
-        description = ''
-          Automatically disallow all clients using <literal>#</literal>
-          or <literal>+</literal> in their name/id.
+        description = mdDoc ''
+          Automatically disallow all clients using `#`
+          or `+` in their name/id.
         '';
         default = true;
       };
 
       options = mkOption {
         type = attrsOf optionType;
-        description = ''
-          Options for the auth plugin. Each key turns into a <literal>auth_opt_*</literal>
+        description = mdDoc ''
+          Options for the auth plugin. Each key turns into a `auth_opt_*`
            line in the config.
         '';
         default = {};
@@ -199,6 +199,7 @@ let
     allow_anonymous = 1;
     allow_zero_length_clientid = 1;
     auto_id_prefix = 1;
+    bind_interface = 1;
     cafile = 1;
     capath = 1;
     certfile = 1;
@@ -238,8 +239,8 @@ let
 
       address = mkOption {
         type = nullOr str;
-        description = ''
-          Address to listen on. Listen on <literal>0.0.0.0</literal>/<literal>::</literal>
+        description = mdDoc ''
+          Address to listen on. Listen on `0.0.0.0`/`::`
           when unset.
         '';
         default = null;
@@ -247,10 +248,10 @@ let
 
       authPlugins = mkOption {
         type = listOf authPluginOptions;
-        description = ''
+        description = mdDoc ''
           Authentication plugin to attach to this listener.
-          Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html">
-          mosquitto.conf documentation</link> for details on authentication plugins.
+          Refer to the [mosquitto.conf documentation](https://mosquitto.org/man/mosquitto-conf-5.html)
+          for details on authentication plugins.
         '';
         default = [];
       };
@@ -295,7 +296,7 @@ let
   };
 
   listenerAsserts = prefix: listener:
-    assertKeysValid prefix freeformListenerKeys listener.settings
+    assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
     ++ userAsserts prefix listener.users
     ++ imap0
       (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
@@ -397,7 +398,7 @@ let
   };
 
   bridgeAsserts = prefix: bridge:
-    assertKeysValid prefix freeformBridgeKeys bridge.settings
+    assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
     ++ [ {
       assertion = length bridge.addresses > 0;
       message = "Bridge ${prefix} needs remote broker addresses";
@@ -471,10 +472,10 @@ let
 
     includeDirs = mkOption {
       type = listOf path;
-      description = ''
+      description = mdDoc ''
         Directories to be scanned for further config files to include.
         Directories will processed in the order given,
-        <literal>*.conf</literal> files in the directory will be
+        `*.conf` files in the directory will be
         read in case-sensistive alphabetical order.
       '';
       default = [];
@@ -526,7 +527,7 @@ let
 
   globalAsserts = prefix: cfg:
     flatten [
-      (assertKeysValid prefix freeformGlobalKeys cfg.settings)
+      (assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
       (imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
       (mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
     ];
@@ -629,9 +630,10 @@ in
                ]));
         RemoveIPC = true;
         RestrictAddressFamilies = [
-          "AF_UNIX"  # for sd_notify() call
+          "AF_UNIX"
           "AF_INET"
           "AF_INET6"
+          "AF_NETLINK"
         ];
         RestrictNamespaces = true;
         RestrictRealtime = true;
diff --git a/nixos/modules/services/networking/mozillavpn.nix b/nixos/modules/services/networking/mozillavpn.nix
new file mode 100644
index 0000000000000..e35ba65314e9b
--- /dev/null
+++ b/nixos/modules/services/networking/mozillavpn.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options.services.mozillavpn.enable = lib.mkOption {
+    type = lib.types.bool;
+    default = false;
+    description = ''
+      Enable the Mozilla VPN daemon.
+    '';
+  };
+
+  config = lib.mkIf config.services.mozillavpn.enable {
+    environment.systemPackages = [ pkgs.mozillavpn ];
+    services.dbus.packages = [ pkgs.mozillavpn ];
+    systemd.packages = [ pkgs.mozillavpn ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ andersk ];
+}
diff --git a/nixos/modules/services/networking/multipath.nix b/nixos/modules/services/networking/multipath.nix
index 1cc2ad1fc849c..1a44184ff6dcc 100644
--- a/nixos/modules/services/networking/multipath.nix
+++ b/nixos/modules/services/networking/multipath.nix
@@ -242,21 +242,6 @@ in {
             '';
           };
 
-          retain_attached_hw_handler = mkOption {
-            type = nullOr (enum [ "yes" "no" ]);
-            default = null; # real default: "yes"
-            description = ''
-              (Obsolete for kernels >= 4.3) If set to "yes" and the SCSI layer has
-              already attached a hardware_handler to the device, multipath will not
-              force the device to use the hardware_handler specified by mutipath.conf.
-              If the SCSI layer has not attached a hardware handler, multipath will
-              continue to use its configured hardware handler.
-
-              Important Note: Linux kernel 4.3 or newer always behaves as if
-              "retain_attached_hw_handler yes" was set.
-            '';
-          };
-
           detect_prio = mkOption {
             type = nullOr (enum [ "yes" "no" ]);
             default = null; # real default: "yes"
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 992678c43ebe2..84b9936aa6235 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -59,6 +59,14 @@ in
         description = "If enabled, start the Murmur Mumble server.";
       };
 
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Murmur Mumble server.
+        '';
+      };
+
       autobanAttempts = mkOption {
         type = types.int;
         default = 10;
@@ -291,6 +299,11 @@ in
       gid             = config.ids.gids.murmur;
     };
 
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+      allowedUDPPorts = [ cfg.port ];
+    };
+
     systemd.services.murmur = {
       description = "Murmur Chat Service";
       wantedBy    = [ "multi-user.target" ];
@@ -306,7 +319,7 @@ in
         Type = if forking then "forking" else "simple";
         PIDFile = mkIf forking "/run/murmur/murmurd.pid";
         EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
-        ExecStart = "${cfg.package}/bin/murmurd -ini /run/murmur/murmurd.ini";
+        ExecStart = "${cfg.package}/bin/mumble-server -ini /run/murmur/murmurd.ini";
         Restart = "always";
         RuntimeDirectory = "murmur";
         RuntimeDirectoryMode = "0700";
diff --git a/nixos/modules/services/networking/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
index 803f0689d1fd1..1509671bc54ae 100644
--- a/nixos/modules/services/networking/mxisd.nix
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -46,6 +46,15 @@ in {
         description = "The mxisd/ma1sd package to use";
       };
 
+      environmentFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to an environment-file which may contain secrets to be
+          substituted via <package>envsubst</package>.
+        '';
+      };
+
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/mxisd";
@@ -118,7 +127,13 @@ in {
         Type = "simple";
         User = "mxisd";
         Group = "mxisd";
-        ExecStart = "${cfg.package}/bin/${executable} -c ${configFile}";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        ExecStart = "${cfg.package}/bin/${executable} -c ${cfg.dataDir}/mxisd-config.yaml";
+        ExecStartPre = "${pkgs.writeShellScript "mxisd-substitute-secrets" ''
+          umask 0077
+          ${pkgs.envsubst}/bin/envsubst -o ${cfg.dataDir}/mxisd-config.yaml \
+            -i ${configFile}
+        ''}";
         WorkingDirectory = cfg.dataDir;
         Restart = "on-failure";
       };
diff --git a/nixos/modules/services/networking/nbd.nix b/nixos/modules/services/networking/nbd.nix
new file mode 100644
index 0000000000000..df3358f51876a
--- /dev/null
+++ b/nixos/modules/services/networking/nbd.nix
@@ -0,0 +1,159 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nbd;
+  iniFields = with types; attrsOf (oneOf [ bool int float str ]);
+  # The `[generic]` section must come before all the others in the
+  # config file.  This means we can't just dump an attrset to INI
+  # because that sorts the sections by name.  Instead, we serialize it
+  # on its own first.
+  genericSection = {
+    generic = (cfg.server.extraOptions // {
+      user = "root";
+      group = "root";
+      port = cfg.server.listenPort;
+    } // (optionalAttrs (cfg.server.listenAddress != null) {
+      listenaddr = cfg.server.listenAddress;
+    }));
+  };
+  exportSections =
+    mapAttrs
+      (_: { path, allowAddresses, extraOptions }:
+        extraOptions // {
+          exportname = path;
+        } // (optionalAttrs (allowAddresses != null) {
+          authfile = pkgs.writeText "authfile" (concatStringsSep "\n" allowAddresses);
+        }))
+      cfg.server.exports;
+  serverConfig =
+    pkgs.writeText "nbd-server-config" ''
+      ${lib.generators.toINI {} genericSection}
+      ${lib.generators.toINI {} exportSections}
+    '';
+  splitLists =
+    partition
+      (path: hasPrefix "/dev/" path)
+      (mapAttrsToList (_: { path, ... }: path) cfg.server.exports);
+  allowedDevices = splitLists.right;
+  boundPaths = splitLists.wrong;
+in
+{
+  options = {
+    services.nbd = {
+      server = {
+        enable = mkEnableOption "the Network Block Device (nbd) server";
+
+        listenPort = mkOption {
+          type = types.port;
+          default = 10809;
+          description = "Port to listen on. The port is NOT automatically opened in the firewall.";
+        };
+
+        extraOptions = mkOption {
+          type = iniFields;
+          default = {
+            allowlist = false;
+          };
+          description = ''
+            Extra options for the server. See
+            <citerefentry><refentrytitle>nbd-server</refentrytitle>
+            <manvolnum>5</manvolnum></citerefentry>.
+          '';
+        };
+
+        exports = mkOption {
+          description = "Files or block devices to make available over the network.";
+          default = { };
+          type = with types; attrsOf
+            (submodule {
+              options = {
+                path = mkOption {
+                  type = str;
+                  description = "File or block device to export.";
+                  example = "/dev/sdb1";
+                };
+
+                allowAddresses = mkOption {
+                  type = nullOr (listOf str);
+                  default = null;
+                  example = [ "10.10.0.0/24" "127.0.0.1" ];
+                  description = "IPs and subnets that are authorized to connect for this device. If not specified, the server will allow all connections.";
+                };
+
+                extraOptions = mkOption {
+                  type = iniFields;
+                  default = {
+                    flush = true;
+                    fua = true;
+                  };
+                  description = ''
+                    Extra options for this export. See
+                    <citerefentry><refentrytitle>nbd-server</refentrytitle>
+                    <manvolnum>5</manvolnum></citerefentry>.
+                  '';
+                };
+              };
+            });
+        };
+
+        listenAddress = mkOption {
+          type = with types; nullOr str;
+          description = "Address to listen on. If not specified, the server will listen on all interfaces.";
+          default = null;
+          example = "10.10.0.1";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.server.enable {
+    assertions = [
+      {
+        assertion = !(cfg.server.exports ? "generic");
+        message = "services.nbd.server exports must not be named 'generic'";
+      }
+    ];
+
+    boot.kernelModules = [ "nbd" ];
+
+    systemd.services.nbd-server = {
+      after = [ "network-online.target" ];
+      before = [ "multi-user.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}";
+        Type = "forking";
+
+        DeviceAllow = map (path: "${path} rw") allowedDevices;
+        BindPaths = boundPaths;
+
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = false;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "noaccess";
+        ProtectSystem = "strict";
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ncdns.nix b/nixos/modules/services/networking/ncdns.nix
index 82c285d051607..c8d1b6718e2e2 100644
--- a/nixos/modules/services/networking/ncdns.nix
+++ b/nixos/modules/services/networking/ncdns.nix
@@ -58,7 +58,7 @@ in
 
       address = mkOption {
         type = types.str;
-        default = "127.0.0.1";
+        default = "[::1]";
         description = ''
           The IP address the ncdns resolver will bind to.  Leave this unchanged
           if you do not wish to directly expose the resolver.
@@ -202,7 +202,7 @@ in
   config = mkIf cfg.enable {
 
     services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveNamecoin {
-      forwardZonesRecurse.bit = "127.0.0.1:${toString cfg.port}";
+      forwardZonesRecurse.bit = "${cfg.address}:${toString cfg.port}";
       luaConfig =
         if cfg.dnssec.enable
           then ''readTrustAnchorsFromFile("${cfg.dnssec.keys.public}")''
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
index de4439415cf68..c83cd9d521c6d 100644
--- a/nixos/modules/services/networking/nebula.nix
+++ b/nixos/modules/services/networking/nebula.nix
@@ -192,6 +192,7 @@ in
                 Group = networkId;
               })
             ];
+            unitConfig.StartLimitIntervalSec = 0; # ensure Restart=always is always honoured (networks can go down for arbitrarily long)
           };
         }) enabledNetworks);
 
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index a9801036b00cd..242afd548df3b 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -5,18 +5,6 @@ with lib;
 let
   cfg = config.networking.networkmanager;
 
-  basePackages = with pkgs; [
-    modemmanager
-    networkmanager
-    networkmanager-fortisslvpn
-    networkmanager-iodine
-    networkmanager-l2tp
-    networkmanager-openconnect
-    networkmanager-openvpn
-    networkmanager-vpnc
-    networkmanager-sstp
-   ] ++ optional (!delegateWireless && !enableIwd) wpa_supplicant;
-
   delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [];
 
   enableIwd = cfg.wifi.backend == "iwd";
@@ -145,6 +133,15 @@ let
     '';
   };
 
+  packages = [
+    pkgs.modemmanager
+    pkgs.networkmanager
+  ]
+  ++ cfg.plugins
+  ++ lib.optionals (!delegateWireless && !enableIwd) [
+    pkgs.wpa_supplicant
+  ];
+
 in {
 
   meta = {
@@ -227,17 +224,33 @@ in {
         '';
       };
 
-      packages = mkOption {
-        type = types.listOf types.package;
+      plugins = mkOption {
+        type =
+          let
+            networkManagerPluginPackage = types.package // {
+              description = "NetworkManager plug-in";
+              check =
+                p:
+                lib.assertMsg
+                  (types.package.check p
+                    && p ? networkManagerPlugin
+                    && lib.isString p.networkManagerPlugin)
+                  ''
+                    Package ‘${p.name}’, is not a NetworkManager plug-in.
+                    Those need to have a ‘networkManagerPlugin’ attribute.
+                  '';
+            };
+          in
+          types.listOf networkManagerPluginPackage;
         default = [ ];
         description = ''
-          Extra packages that provide NetworkManager plugins.
+          List of NetworkManager plug-ins to enable.
+          Some plug-ins are enabled by the NetworkManager module by default.
         '';
-        apply = list: basePackages ++ list;
       };
 
       dhcp = mkOption {
-        type = types.enum [ "dhclient" "dhcpcd" "internal" ];
+        type = types.enum [ "dhcpcd" "internal" ];
         default = "internal";
         description = ''
           Which program (or internal library) should be used for DHCP.
@@ -380,7 +393,7 @@ in {
           </para><para>
           If you enable this option the
           <literal>networkmanager_strongswan</literal> plugin will be added to
-          the <option>networking.networkmanager.packages</option> option
+          the <option>networking.networkmanager.plugins</option> option
           so you don't need to to that yourself.
         '';
       };
@@ -399,6 +412,9 @@ in {
   };
 
   imports = [
+    (mkRenamedOptionModule
+      [ "networking" "networkmanager" "packages" ]
+      [ "networking" "networkmanager" "plugins" ])
     (mkRenamedOptionModule [ "networking" "networkmanager" "useDnsmasq" ] [ "networking" "networkmanager" "dns" ])
     (mkRemovedOptionModule ["networking" "networkmanager" "dynamicHosts"] ''
       This option was removed because allowing (multiple) regular users to
@@ -426,31 +442,12 @@ in {
 
     hardware.wirelessRegulatoryDatabase = true;
 
-    environment.etc = with pkgs; {
-      "NetworkManager/NetworkManager.conf".source = configFile;
-
-      "NetworkManager/VPN/nm-openvpn-service.name".source =
-        "${networkmanager-openvpn}/lib/NetworkManager/VPN/nm-openvpn-service.name";
-
-      "NetworkManager/VPN/nm-vpnc-service.name".source =
-        "${networkmanager-vpnc}/lib/NetworkManager/VPN/nm-vpnc-service.name";
-
-      "NetworkManager/VPN/nm-openconnect-service.name".source =
-        "${networkmanager-openconnect}/lib/NetworkManager/VPN/nm-openconnect-service.name";
-
-      "NetworkManager/VPN/nm-fortisslvpn-service.name".source =
-        "${networkmanager-fortisslvpn}/lib/NetworkManager/VPN/nm-fortisslvpn-service.name";
-
-      "NetworkManager/VPN/nm-l2tp-service.name".source =
-        "${networkmanager-l2tp}/lib/NetworkManager/VPN/nm-l2tp-service.name";
-
-      "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";
-
+    environment.etc = {
+        "NetworkManager/NetworkManager.conf".source = configFile;
       }
+      // builtins.listToAttrs (map (pkg: nameValuePair "NetworkManager/${pkg.networkManagerPlugin}" {
+        source = "${pkg}/lib/NetworkManager/${pkg.networkManagerPlugin}";
+      }) cfg.plugins)
       // optionalAttrs cfg.enableFccUnlock
          {
            "ModemManager/fcc-unlock.d".source =
@@ -460,18 +457,13 @@ in {
          {
            "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
          }
-      // optionalAttrs cfg.enableStrongSwan
-         {
-           "NetworkManager/VPN/nm-strongswan-service.name".source =
-             "${pkgs.networkmanager_strongswan}/lib/NetworkManager/VPN/nm-strongswan-service.name";
-         }
       // listToAttrs (lib.imap1 (i: s:
          {
             name = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
             value = { mode = "0544"; inherit (s) source; };
          }) cfg.dispatcherScripts);
 
-    environment.systemPackages = cfg.packages;
+    environment.systemPackages = packages;
 
     users.groups = {
       networkmanager.gid = config.ids.gids.networkmanager;
@@ -490,14 +482,13 @@ in {
       };
     };
 
-    systemd.packages = cfg.packages;
+    systemd.packages = packages;
 
     systemd.tmpfiles.rules = [
       "d /etc/NetworkManager/system-connections 0700 root root -"
       "d /etc/ipsec.d 0700 root root -"
       "d /var/lib/NetworkManager-fortisslvpn 0700 root root -"
 
-      "d /var/lib/dhclient 0755 root root -"
       "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
     ];
 
@@ -534,8 +525,20 @@ in {
         useDHCP = false;
       })
 
+      {
+        networkmanager.plugins = with pkgs; [
+          networkmanager-fortisslvpn
+          networkmanager-iodine
+          networkmanager-l2tp
+          networkmanager-openconnect
+          networkmanager-openvpn
+          networkmanager-vpnc
+          networkmanager-sstp
+        ];
+      }
+
       (mkIf cfg.enableStrongSwan {
-        networkmanager.packages = [ pkgs.networkmanager_strongswan ];
+        networkmanager.plugins = [ pkgs.networkmanager_strongswan ];
       })
 
       (mkIf enableIwd {
@@ -556,12 +559,13 @@ in {
 
     boot.kernelModules = [ "ctr" ];
 
+    security.polkit.enable = true;
     security.polkit.extraConfig = polkitConf;
 
-    services.dbus.packages = cfg.packages
+    services.dbus.packages = packages
       ++ optional cfg.enableStrongSwan pkgs.strongswanNM
       ++ optional (cfg.dns == "dnsmasq") pkgs.dnsmasq;
 
-    services.udev.packages = cfg.packages;
+    services.udev.packages = packages;
   };
 }
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index cf6c9661dc1b0..a51fc53453424 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -194,19 +194,8 @@ let
                        zone.children
       );
 
-  # fighting infinite recursion
-  zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true;
-  zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false;
-  zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false;
-  zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false;
-  zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false;
-  zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false;
-  zoneOptions6 = zoneOptionsRaw // childConfig null         false;
-
-  childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; };
-
   # options are ordered alphanumerically
-  zoneOptionsRaw = types.submodule {
+  zoneOptions = types.submodule {
     options = {
 
       allowAXFRFallback = mkOption {
@@ -246,6 +235,13 @@ let
       };
 
       children = mkOption {
+        # TODO: This relies on the fact that `types.anything` doesn't set any
+        # values of its own to any defaults, because in the above zoneConfigs',
+        # values from children override ones from parents, but only if the
+        # attributes are defined. Because of this, we can't replace the element
+        # type here with `zoneConfigs`, since that would set all the attributes
+        # to default values, breaking the parent inheriting function.
+        type = types.attrsOf types.anything;
         default = {};
         description = ''
           Children zones inherit all options of their parents. Attributes
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
index 77a004e8ab3a5..022fc923edaa3 100644
--- a/nixos/modules/services/networking/ntopng.nix
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -6,7 +6,13 @@ let
 
   cfg = config.services.ntopng;
   opt = options.services.ntopng;
-  redisCfg = config.services.redis;
+
+  createRedis = cfg.redis.createInstance != null;
+  redisService =
+    if cfg.redis.createInstance == "" then
+      "redis.service"
+    else
+      "redis-${cfg.redis.createInstance}.service";
 
   configFile = if cfg.configText != "" then
     pkgs.writeText "ntopng.conf" ''
@@ -15,8 +21,10 @@ let
     else
     pkgs.writeText "ntopng.conf" ''
       ${concatStringsSep " " (map (e: "--interface=" + e) cfg.interfaces)}
-      --http-port=${toString cfg.http-port}
-      --redis=localhost:${toString redisCfg.port}
+      --http-port=${toString cfg.httpPort}
+      --redis=${cfg.redis.address}
+      --data-dir=/var/lib/ntopng
+      --user=ntopng
       ${cfg.extraConfig}
     '';
 
@@ -24,6 +32,10 @@ in
 
 {
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "ntopng" "http-port" ] [ "services" "ntopng" "httpPort" ])
+  ];
+
   options = {
 
     services.ntopng = {
@@ -56,7 +68,7 @@ in
         '';
       };
 
-      http-port = mkOption {
+      httpPort = mkOption {
         default = 3000;
         type = types.int;
         description = ''
@@ -64,6 +76,24 @@ in
         '';
       };
 
+      redis.address = mkOption {
+        type = types.str;
+        example = literalExpression "config.services.redis.ntopng.unixSocket";
+        description = ''
+          Redis address - may be a Unix socket or a network host and port.
+        '';
+      };
+
+      redis.createInstance = mkOption {
+        type = types.nullOr types.str;
+        default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else "";
+        description = ''
+          Local Redis instance name. Set to <literal>null</literal> to disable
+          local Redis instance. Defaults to <literal>""</literal> for
+          <literal>system.stateVersion</literal> older than 22.05.
+        '';
+      };
+
       configText = mkOption {
         default = "";
         example = ''
@@ -95,23 +125,36 @@ in
   config = mkIf cfg.enable {
 
     # ntopng uses redis for data storage
-    services.redis.enable = true;
+    services.ntopng.redis.address =
+      mkIf createRedis config.services.redis.servers.${cfg.redis.createInstance}.unixSocket;
+
+    services.redis.servers = mkIf createRedis {
+      ${cfg.redis.createInstance} = {
+        enable = true;
+        user = mkIf (cfg.redis.createInstance == "ntopng") "ntopng";
+      };
+    };
 
     # nice to have manual page and ntopng command in PATH
     environment.systemPackages = [ pkgs.ntopng ];
 
+    systemd.tmpfiles.rules = [ "d /var/lib/ntopng 0700 ntopng ntopng -" ];
+
     systemd.services.ntopng = {
       description = "Ntopng Network Monitor";
-      requires = [ "redis.service" ];
-      after = [ "network.target" "redis.service" ];
+      requires = optional createRedis redisService;
+      after = [ "network.target" ] ++ optional createRedis redisService;
       wantedBy = [ "multi-user.target" ];
-      preStart = "mkdir -p /var/lib/ntopng/";
       serviceConfig.ExecStart = "${pkgs.ntopng}/bin/ntopng ${configFile}";
       unitConfig.Documentation = "man:ntopng(8)";
     };
 
-    # ntopng drops priveleges to user "nobody" and that user is already defined
-    # in users-groups.nix.
+    users.extraUsers.ntopng = {
+      group = "ntopng";
+      isSystemUser = true;
+    };
+
+    users.extraGroups.ntopng = { };
   };
 
 }
diff --git a/nixos/modules/services/networking/openconnect.nix b/nixos/modules/services/networking/openconnect.nix
new file mode 100644
index 0000000000000..bc873b2198bcc
--- /dev/null
+++ b/nixos/modules/services/networking/openconnect.nix
@@ -0,0 +1,143 @@
+{ config, lib, options, pkgs, ... }:
+with lib;
+let
+  cfg = config.networking.openconnect;
+  openconnect = cfg.package;
+  pkcs11 = types.strMatching "pkcs11:.+" // {
+    name = "pkcs11";
+    description = "PKCS#11 URI";
+  };
+  interfaceOptions = {
+    options = {
+      autoStart = mkOption {
+        default = true;
+        description = "Whether this VPN connection should be started automatically.";
+        type = types.bool;
+      };
+
+      gateway = mkOption {
+        description = "Gateway server to connect to.";
+        example = "gateway.example.com";
+        type = types.str;
+      };
+
+      protocol = mkOption {
+        description = "Protocol to use.";
+        example = "anyconnect";
+        type =
+          types.enum [ "anyconnect" "array" "nc" "pulse" "gp" "f5" "fortinet" ];
+      };
+
+      user = mkOption {
+        description = "Username to authenticate with.";
+        example = "example-user";
+        type = types.nullOr types.str;
+      };
+
+      # Note: It does not make sense to provide a way to declaratively
+      # set an authentication cookie, because they have to be requested
+      # for every new connection and would only work once.
+      passwordFile = mkOption {
+        description = ''
+          File containing the password to authenticate with. This
+          is passed to <code>openconnect</code> via the
+          <code>--passwd-on-stdin</code> option.
+        '';
+        default = null;
+        example = "/var/lib/secrets/openconnect-passwd";
+        type = types.nullOr types.path;
+      };
+
+      certificate = mkOption {
+        description = "Certificate to authenticate with.";
+        default = null;
+        example = "/var/lib/secrets/openconnect_certificate.pem";
+        type = with types; nullOr (either path pkcs11);
+      };
+
+      privateKey = mkOption {
+        description = "Private key to authenticate with.";
+        example = "/var/lib/secrets/openconnect_private_key.pem";
+        default = null;
+        type = with types; nullOr (either path pkcs11);
+      };
+
+      extraOptions = mkOption {
+        description = ''
+          Extra config to be appended to the interface config. It should
+          contain long-format options as would be accepted on the command
+          line by <code>openconnect</code>
+          (see https://www.infradead.org/openconnect/manual.html).
+          Non-key-value options like <code>deflate</code> can be used by
+          declaring them as booleans, i. e. <code>deflate = true;</code>.
+        '';
+        default = { };
+        example = {
+          compression = "stateless";
+
+          no-http-keepalive = true;
+          no-dtls = true;
+        };
+        type = with types; attrsOf (either str bool);
+      };
+    };
+  };
+  generateExtraConfig = extra_cfg:
+    strings.concatStringsSep "\n" (attrsets.mapAttrsToList
+      (name: value: if (value == true) then name else "${name}=${value}")
+      (attrsets.filterAttrs (_: value: value != false) extra_cfg));
+  generateConfig = name: icfg:
+    pkgs.writeText "config" ''
+      interface=${name}
+      ${optionalString (icfg.user != null) "user=${icfg.user}"}
+      ${optionalString (icfg.passwordFile != null) "passwd-on-stdin"}
+      ${optionalString (icfg.certificate != null)
+      "certificate=${icfg.certificate}"}
+      ${optionalString (icfg.privateKey != null) "sslkey=${icfg.privateKey}"}
+
+      ${generateExtraConfig icfg.extraOptions}
+    '';
+  generateUnit = name: icfg: {
+    description = "OpenConnect Interface - ${name}";
+    requires = [ "network-online.target" ];
+    after = [ "network.target" "network-online.target" ];
+    wantedBy = optional icfg.autoStart "multi-user.target";
+
+    serviceConfig = {
+      Type = "simple";
+      ExecStart = "${openconnect}/bin/openconnect --config=${
+          generateConfig name icfg
+        } ${icfg.gateway}";
+      StandardInput = "file:${icfg.passwordFile}";
+
+      ProtectHome = true;
+    };
+  };
+in {
+  options.networking.openconnect = {
+    package = mkPackageOption pkgs "openconnect" { };
+
+    interfaces = mkOption {
+      description = "OpenConnect interfaces.";
+      default = { };
+      example = {
+        openconnect0 = {
+          gateway = "gateway.example.com";
+          protocol = "anyconnect";
+          user = "example-user";
+          passwordFile = "/var/lib/secrets/openconnect-passwd";
+        };
+      };
+      type = with types; attrsOf (submodule interfaceOptions);
+    };
+  };
+
+  config = {
+    systemd.services = mapAttrs' (name: value: {
+      name = "openconnect-${name}";
+      value = generateUnit name value;
+    }) cfg.interfaces;
+  };
+
+  meta.maintainers = with maintainers; [ alyaeanyx ];
+}
diff --git a/nixos/modules/services/networking/openfire.nix b/nixos/modules/services/networking/openfire.nix
deleted file mode 100644
index fe0499d523231..0000000000000
--- a/nixos/modules/services/networking/openfire.nix
+++ /dev/null
@@ -1,56 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-{
-  ###### interface
-
-  options = {
-
-    services.openfire = {
-
-      enable = mkEnableOption "OpenFire XMPP server";
-
-      usePostgreSQL = mkOption {
-        type = types.bool;
-        default = true;
-        description = "
-          Whether you use PostgreSQL service for your storage back-end.
-        ";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.services.openfire.enable {
-
-    assertions = singleton
-      { assertion = !(config.services.openfire.usePostgreSQL -> config.services.postgresql.enable);
-        message = "OpenFire configured to use PostgreSQL but services.postgresql.enable is not enabled.";
-      };
-
-    systemd.services.openfire = {
-      description = "OpenFire XMPP server";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "networking.target" ] ++
-        optional config.services.openfire.usePostgreSQL "postgresql.service";
-      path = with pkgs; [ jre openfire coreutils which gnugrep gawk gnused ];
-      script = ''
-        export HOME=/tmp
-        mkdir /var/log/openfire || true
-        mkdir /etc/openfire || true
-        for i in ${pkgs.openfire}/conf.inst/*; do
-            if ! test -f /etc/openfire/$(basename $i); then
-                cp $i /etc/openfire/
-            fi
-        done
-        openfire start
-      ''; # */
-    };
-  };
-
-}
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
index 0579d314a9ba6..a986f83141c4d 100644
--- a/nixos/modules/services/networking/pdns-recursor.nix
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -30,10 +30,10 @@ in {
     enable = mkEnableOption "PowerDNS Recursor, a recursive DNS server";
 
     dns.address = mkOption {
-      type = types.str;
-      default = "0.0.0.0";
+      type = oneOrMore types.str;
+      default = [ "::" "0.0.0.0" ];
       description = ''
-        IP address Recursor DNS server will bind to.
+        IP addresses Recursor DNS server will bind to.
       '';
     };
 
@@ -47,8 +47,12 @@ in {
 
     dns.allowFrom = mkOption {
       type = types.listOf types.str;
-      default = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" ];
-      example = [ "0.0.0.0/0" ];
+      default = [
+        "127.0.0.0/8" "10.0.0.0/8" "100.64.0.0/10"
+        "169.254.0.0/16" "192.168.0.0/16" "172.16.0.0/12"
+        "::1/128" "fc00::/7" "fe80::/10"
+      ];
+      example = [ "0.0.0.0/0" "::/0" ];
       description = ''
         IP address ranges of clients allowed to make DNS queries.
       '';
@@ -72,7 +76,8 @@ in {
 
     api.allowFrom = mkOption {
       type = types.listOf types.str;
-      default = [ "0.0.0.0/0" ];
+      default = [ "127.0.0.1" "::1" ];
+      example = [ "0.0.0.0/0" "::/0" ];
       description = ''
         IP address ranges of clients allowed to make API requests.
       '';
@@ -96,7 +101,7 @@ in {
 
     forwardZonesRecurse = mkOption {
       type = types.attrs;
-      example = { eth = "127.0.0.1:5353"; };
+      example = { eth = "[::1]:5353"; };
       default = {};
       description = ''
         DNS zones to be forwarded to other recursive servers.
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
index 8cae61b835431..b035698456c05 100644
--- a/nixos/modules/services/networking/powerdns.nix
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -24,14 +24,14 @@ in {
 
   config = mkIf cfg.enable {
 
-    systemd.packages = [ pkgs.powerdns ];
+    systemd.packages = [ pkgs.pdns ];
 
     systemd.services.pdns = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" "mysql.service" "postgresql.service" "openldap.service" ];
 
       serviceConfig = {
-        ExecStart = [ "" "${pkgs.powerdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
+        ExecStart = [ "" "${pkgs.pdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
       };
     };
 
diff --git a/nixos/modules/services/networking/pptpd.nix b/nixos/modules/services/networking/pptpd.nix
index 3e7753b9dd352..423e14e998f85 100644
--- a/nixos/modules/services/networking/pptpd.nix
+++ b/nixos/modules/services/networking/pptpd.nix
@@ -108,7 +108,7 @@ with lib;
         #username	pptpd	password	*
         EOF
 
-        chown root.root "$secrets"
+        chown root:root "$secrets"
         chmod 600 "$secrets"
       '';
 
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
index ae9258b27122f..513509eaca3ab 100644
--- a/nixos/modules/services/networking/prayer.nix
+++ b/nixos/modules/services/networking/prayer.nix
@@ -82,7 +82,7 @@ in
       serviceConfig.Type = "forking";
       preStart = ''
         mkdir -m 0755 -p ${stateDir}
-        chown ${prayerUser}.${prayerGroup} ${stateDir}
+        chown ${prayerUser}:${prayerGroup} ${stateDir}
       '';
       script = "${prayer}/sbin/prayer --config-file=${prayerCfg}";
     };
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index 42596ccfefd97..9e8db04e62241 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -511,8 +511,13 @@ in
 
       dataDir = mkOption {
         type = types.path;
-        description = "Directory where Prosody stores its data";
         default = "/var/lib/prosody";
+        description = ''
+          The prosody home directory used to store all data. If left as the default value
+          this directory will automatically be created before the prosody server starts, otherwise
+          you are responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
+        '';
       };
 
       disco_items = mkOption {
@@ -524,13 +529,29 @@ in
       user = mkOption {
         type = types.str;
         default = "prosody";
-        description = "User account under which prosody runs.";
+        description = ''
+          User account under which prosody runs.
+
+          <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the prosody service starts.
+          </para></note>
+        '';
       };
 
       group = mkOption {
         type = types.str;
         default = "prosody";
-        description = "Group account under which prosody runs.";
+        description = ''
+          Group account under which prosody runs.
+
+          <note><para>
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the group exists before the prosody service starts.
+          </para></note>
+        '';
       };
 
       allowRegistration = mkOption {
@@ -820,6 +841,7 @@ in
         '') cfg.muc}
 
       ${ lib.optionalString (cfg.uploadHttp != null) ''
+        -- TODO: think about migrating this to mod-http_file_share instead.
         Component ${toLua cfg.uploadHttp.domain} "http_upload"
             http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
             http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
@@ -838,9 +860,8 @@ in
     users.users.prosody = mkIf (cfg.user == "prosody") {
       uid = config.ids.uids.prosody;
       description = "Prosody user";
-      createHome = true;
       inherit (cfg) group;
-      home = "${cfg.dataDir}";
+      home = cfg.dataDir;
     };
 
     users.groups.prosody = mkIf (cfg.group == "prosody") {
@@ -853,28 +874,33 @@ in
       wants = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
-      serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        Type = "forking";
-        RuntimeDirectory = [ "prosody" ];
-        PIDFile = "/run/prosody/prosody.pid";
-        ExecStart = "${cfg.package}/bin/prosodyctl start";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-
-        MemoryDenyWriteExecute = true;
-        PrivateDevices = true;
-        PrivateMounts = true;
-        PrivateTmp = true;
-        ProtectControlGroups = true;
-        ProtectHome = true;
-        ProtectHostname = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        RestrictSUIDSGID = true;
-      };
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          Type = "forking";
+          RuntimeDirectory = [ "prosody" ];
+          PIDFile = "/run/prosody/prosody.pid";
+          ExecStart = "${cfg.package}/bin/prosodyctl start";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+        }
+        (mkIf (cfg.dataDir == "/var/lib/prosody") {
+          StateDirectory = "prosody";
+        })
+      ];
     };
 
   };
diff --git a/nixos/modules/services/networking/r53-ddns.nix b/nixos/modules/services/networking/r53-ddns.nix
new file mode 100644
index 0000000000000..a8839762d530d
--- /dev/null
+++ b/nixos/modules/services/networking/r53-ddns.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.r53-ddns;
+  pkg = pkgs.r53-ddns;
+in
+{
+  options = {
+    services.r53-ddns = {
+
+      enable = mkEnableOption "r53-ddyns";
+
+      interval = mkOption {
+        type = types.str;
+        default = "15min";
+        description = "How often to update the entry";
+      };
+
+      zoneID = mkOption {
+        type = types.str;
+        description = "The ID of your zone in Route53";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = "The name of your domain in Route53";
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        description = ''
+          Manually specify the hostname. Otherwise the tool will try to use the name
+          returned by the OS (Call to gethostname)
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.str;
+        description = ''
+          File containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
+          in the format of an EnvironmentFile as described by systemd.exec(5)
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.timers.r53-ddns = {
+      description = "r53-ddns timer";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnBootSec = cfg.interval;
+        OnUnitActiveSec = cfg.interval;
+      };
+    };
+
+    systemd.services.r53-ddns = {
+      description = "r53-ddns service";
+      serviceConfig = {
+        ExecStart = "${pkg}/bin/r53-ddns -zone-id ${cfg.zoneID} -domain ${cfg.domain}"
+          + lib.optionalString (cfg.hostname != null) " -hostname ${cfg.hostname}";
+        EnvironmentFile = "${cfg.environmentFile}";
+        DynamicUser = true;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index c6c40777ed7cf..227bafc1d0e8c 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -164,7 +164,7 @@ in {
         StateDirectoryMode = "0750";
         # Hardening
         CapabilityBoundingSet = [ "" ];
-        DeviceAllow = [ "/dev/stdin" ];
+        DeviceAllow = [ "/dev/stdin" "/dev/urandom" ];
         DevicePolicy = "strict";
         IPAddressAllow = mkIf bindLocalhost "localhost";
         IPAddressDeny = mkIf bindLocalhost "any";
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
index 6e8db55bbf0d1..fb3eb71a8ce19 100644
--- a/nixos/modules/services/networking/radvd.nix
+++ b/nixos/modules/services/networking/radvd.nix
@@ -16,9 +16,9 @@ in
 
   ###### interface
 
-  options = {
+  options.services.radvd = {
 
-    services.radvd.enable = mkOption {
+    enable = mkOption {
       type = types.bool;
       default = false;
       description =
@@ -32,7 +32,16 @@ in
         '';
     };
 
-    services.radvd.config = mkOption {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.radvd;
+      defaultText = literalExpression "pkgs.radvd";
+      description = ''
+        The RADVD package to use for the RADVD service.
+      '';
+    };
+
+    config = mkOption {
       type = types.lines;
       example =
         ''
@@ -67,7 +76,7 @@ in
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
         serviceConfig =
-          { ExecStart = "@${pkgs.radvd}/bin/radvd radvd -n -u radvd -C ${confFile}";
+          { ExecStart = "@${cfg.package}/bin/radvd radvd -n -u radvd -C ${confFile}";
             Restart = "always";
           };
       };
diff --git a/nixos/modules/services/networking/routedns.nix b/nixos/modules/services/networking/routedns.nix
new file mode 100644
index 0000000000000..e0f5eedd2c8e5
--- /dev/null
+++ b/nixos/modules/services/networking/routedns.nix
@@ -0,0 +1,84 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.routedns;
+  settingsFormat = pkgs.formats.toml { };
+in
+{
+  options.services.routedns = {
+    enable = mkEnableOption "RouteDNS - DNS stub resolver, proxy and router";
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      example = literalExpression ''
+        {
+          resolvers.cloudflare-dot = {
+            address = "1.1.1.1:853";
+            protocol = "dot";
+          };
+          groups.cloudflare-cached = {
+            type = "cache";
+            resolvers = ["cloudflare-dot"];
+          };
+          listeners.local-udp = {
+            address = "127.0.0.1:53";
+            protocol = "udp";
+            resolver = "cloudflare-cached";
+          };
+          listeners.local-tcp = {
+            address = "127.0.0.1:53";
+            protocol = "tcp";
+            resolver = "cloudflare-cached";
+          };
+        }
+      '';
+      description = ''
+        Configuration for RouteDNS, see <link xlink:href="https://github.com/folbricht/routedns/blob/master/doc/configuration.md"/>
+        for more information.
+      '';
+    };
+
+    configFile = mkOption {
+      default = settingsFormat.generate "routedns.toml" cfg.settings;
+      defaultText = "A RouteDNS configuration file automatically generated by values from services.routedns.*";
+      type = types.path;
+      example = literalExpression ''"''${pkgs.routedns}/cmd/routedns/example-config/use-case-1.toml"'';
+      description = "Path to RouteDNS TOML configuration file.";
+    };
+
+    package = mkOption {
+      default = pkgs.routedns;
+      defaultText = literalExpression "pkgs.routedns";
+      type = types.package;
+      description = "RouteDNS package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.routedns = {
+      description = "RouteDNS - DNS stub resolver, proxy and router";
+      after = [ "network.target" ]; # in case a bootstrap resolver is used, this might fail a few times until the respective server is actually reachable
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      startLimitIntervalSec = 30;
+      startLimitBurst = 5;
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = "5s";
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        DynamicUser = true;
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        NoNewPrivileges = true;
+        ExecStart = "${getBin cfg.package}/bin/routedns -l 4 ${cfg.configFile}";
+      };
+    };
+  };
+  meta.maintainers = with maintainers; [ jsimonetti ];
+}
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index b73f255eb9dd2..10dc27da70098 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -143,6 +143,7 @@ in
             disable-logging = true;
             http = ":8080";                   # serve via HTTP...
             socket = "/run/searx/searx.sock"; # ...or UNIX socket
+            chmod-socket = "660";             # allow the searx group to read/write to the socket
           }
         '';
         description = ''
@@ -220,7 +221,12 @@ in
         lazy-apps = true;
         enable-threads = true;
         module = "searx.webapp";
-        env = [ "SEARX_SETTINGS_PATH=${cfg.settingsFile}" ];
+        env = [
+          "SEARX_SETTINGS_PATH=${cfg.settingsFile}"
+          # searxng compatiblity https://github.com/searxng/searxng/issues/1519
+          "SEARXNG_SETTINGS_PATH=${cfg.settingsFile}"
+        ];
+        buffer-size = 32768;
         pythonPackages = self: [ cfg.package ];
       } // cfg.uwsgiConfig;
     };
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
index a45ef148544f9..57825945d9f76 100644
--- a/nixos/modules/services/networking/shellhub-agent.nix
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -1,31 +1,37 @@
 { config, lib, pkgs, ... }:
 
 with lib;
+
 let
   cfg = config.services.shellhub-agent;
-in {
-
+in
+{
   ###### interface
 
   options = {
 
     services.shellhub-agent = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
+      enable = mkEnableOption "ShellHub Agent daemon";
+
+      package = mkPackageOption pkgs "shellhub-agent" { };
+
+      preferredHostname = mkOption {
+        type = types.str;
+        default = "";
         description = ''
-          Whether to enable the ShellHub Agent daemon, which allows
-          secure remote logins.
+          Set the device preferred hostname. This provides a hint to
+          the server to use this as hostname if it is available.
         '';
       };
 
-      package = mkOption {
-        type = types.package;
-        default = pkgs.shellhub-agent;
-        defaultText = literalExpression "pkgs.shellhub-agent";
+      keepAliveInterval = mkOption {
+        type = types.int;
+        default = 30;
         description = ''
-          Which ShellHub Agent package to use.
+          Determine the interval to send the keep alive message to
+          the server. This has a direct impact of the bandwidth
+          used by the device.
         '';
       };
 
@@ -74,9 +80,13 @@ in {
         "time-sync.target"
       ];
 
-      environment.SERVER_ADDRESS = cfg.server;
-      environment.PRIVATE_KEY = cfg.privateKey;
-      environment.TENANT_ID = cfg.tenantId;
+      environment = {
+        SHELLHUB_SERVER_ADDRESS = cfg.server;
+        SHELLHUB_PRIVATE_KEY = cfg.privateKey;
+        SHELLHUB_TENANT_ID = cfg.tenantId;
+        SHELLHUB_KEEPALIVE_INTERVAL = toString cfg.keepAliveInterval;
+        SHELLHUB_PREFERRED_HOSTNAME = cfg.preferredHostname;
+      };
 
       serviceConfig = {
         # The service starts sessions for different users.
@@ -85,7 +95,6 @@ in {
         ExecStart = "${cfg.package}/bin/agent";
       };
     };
-
-    environment.systemPackages = [ cfg.package ];
   };
 }
+
diff --git a/nixos/modules/services/networking/snowflake-proxy.nix b/nixos/modules/services/networking/snowflake-proxy.nix
new file mode 100644
index 0000000000000..2124644ed9b5e
--- /dev/null
+++ b/nixos/modules/services/networking/snowflake-proxy.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snowflake-proxy;
+in
+{
+  options = {
+    services.snowflake-proxy = {
+      enable = mkEnableOption "System to defeat internet censorship";
+
+      broker = mkOption {
+        description = "Broker URL (default \"https://snowflake-broker.torproject.net/\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+
+      capacity = mkOption {
+        description = "Limits the amount of maximum concurrent clients allowed.";
+        type = with types; nullOr int;
+        default = null;
+      };
+
+      relay = mkOption {
+        description = "websocket relay URL (default \"wss://snowflake.bamsoftware.com/\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+
+      stun = mkOption {
+        description = "STUN broker URL (default \"stun:stun.stunprotocol.org:3478\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.snowflake-proxy = {
+      wantedBy = [ "network-online.target" ];
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.snowflake}/bin/proxy " + concatStringsSep " " (
+            optional (cfg.broker != null) "-broker ${cfg.broker}"
+            ++ optional (cfg.capacity != null) "-capacity ${builtins.toString cfg.capacity}"
+            ++ optional (cfg.relay != null) "-relay ${cfg.relay}"
+            ++ optional (cfg.stun != null) "-stun ${cfg.stun}"
+          );
+
+        # Security Hardening
+        # Refer to systemd.exec(5) for option descriptions.
+        CapabilityBoundingSet = "";
+
+        # implies RemoveIPC=, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=,
+        # ProtectSystem=strict, ProtectHome=read-only
+        DynamicUser = true;
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @mount @obsolete @reboot @swap @privileged @resources";
+        UMask = "0077";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ yayayayaka ];
+}
diff --git a/nixos/modules/services/networking/squid.nix b/nixos/modules/services/networking/squid.nix
index 9d063b92aa1e7..db4f0d26b6f4a 100644
--- a/nixos/modules/services/networking/squid.nix
+++ b/nixos/modules/services/networking/squid.nix
@@ -81,7 +81,9 @@ let
     http_access deny all
 
     # Squid normally listens to port 3128
-    http_port ${toString cfg.proxyPort}
+    http_port ${
+      optionalString (cfg.proxyAddress != null) "${cfg.proxyAddress}:"
+    }${toString cfg.proxyPort}
 
     # Leave coredumps in the first cache dir
     coredump_dir /var/cache/squid
@@ -109,6 +111,19 @@ in
         description = "Whether to run squid web proxy.";
       };
 
+      package = mkOption {
+        default = pkgs.squid;
+        defaultText = literalExpression "pkgs.squid";
+        type = types.package;
+        description = "Squid package to use.";
+      };
+
+      proxyAddress = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "IP address on which squid will listen.";
+      };
+
       proxyPort = mkOption {
         type = types.int;
         default = 3128;
@@ -149,17 +164,21 @@ in
     users.groups.squid = {};
 
     systemd.services.squid = {
-      description = "Squid caching web proxy";
+      description = "Squid caching proxy";
+      documentation = [ "man:squid(8)" ];
       after = [ "network.target" "nss-lookup.target" ];
       wantedBy = [ "multi-user.target"];
       preStart = ''
         mkdir -p "/var/log/squid"
         chown squid:squid "/var/log/squid"
+        ${cfg.package}/bin/squid --foreground -z -f ${squidConfig}
       '';
       serviceConfig = {
-        Type="forking";
         PIDFile="/run/squid.pid";
-        ExecStart  = "${pkgs.squid}/bin/squid -YCs -f ${squidConfig}";
+        ExecStart  = "${cfg.package}/bin/squid --foreground -YCs -f ${squidConfig}";
+        ExecReload="kill -HUP $MAINPID";
+        KillMode="mixed";
+        NotifyAccess="all";
       };
     };
 
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 230ab673a9761..52a50b892ec61 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -293,6 +293,7 @@ in
       kexAlgorithms = mkOption {
         type = types.listOf types.str;
         default = [
+          "sntrup761x25519-sha512@openssh.com"
           "curve25519-sha256"
           "curve25519-sha256@libssh.org"
           "diffie-hellman-group-exchange-sha256"
@@ -301,7 +302,7 @@ in
           Allowed key exchange algorithms
           </para>
           <para>
-          Defaults to recommended settings from both
+          Uses the lower bound recommended in both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
           <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
@@ -441,6 +442,9 @@ in
 
                 ${flip concatMapStrings cfg.hostKeys (k: ''
                   if ! [ -s "${k.path}" ]; then
+                      if ! [ -h "${k.path}" ]; then
+                          rm -f "${k.path}"
+                      fi
                       ssh-keygen \
                         -t "${k.type}" \
                         ${if k ? bits then "-b ${toString k.bits}" else ""} \
diff --git a/nixos/modules/services/networking/supplicant.nix b/nixos/modules/services/networking/supplicant.nix
index eb24130e519a7..e111b311d68f4 100644
--- a/nixos/modules/services/networking/supplicant.nix
+++ b/nixos/modules/services/networking/supplicant.nix
@@ -43,7 +43,7 @@ let
         path = [ pkgs.coreutils ];
 
         preStart = ''
-          ${optionalString (suppl.configFile.path!=null) ''
+          ${optionalString (suppl.configFile.path!=null && suppl.configFile.writable) ''
             (umask 077 && touch -a "${suppl.configFile.path}")
           ''}
           ${optionalString suppl.userControlled.enable ''
@@ -226,10 +226,10 @@ in
               ACTION=="add", SUBSYSTEM=="net", ENV{INTERFACE}=="${i}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="supplicant-${replaceChars [" "] ["-"] iface}.service", TAG+="SUPPLICANT_ASSIGNED"''))}
 
           ${optionalString (hasAttr "WLAN" cfg) ''
-            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-wlan@$result.service"
+            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="/run/current-system/systemd/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-wlan@$result.service"
           ''}
           ${optionalString (hasAttr "LAN" cfg) ''
-            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="lan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-lan@$result.service"
+            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="lan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="/run/current-system/systemd/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-lan@$result.service"
           ''}
         '';
       })];
diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix
index b6faf2d3f7727..7694b4bf990b8 100644
--- a/nixos/modules/services/networking/syncplay.nix
+++ b/nixos/modules/services/networking/syncplay.nix
@@ -61,6 +61,15 @@ in
           Group to use when running Syncplay.
         '';
       };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to the file that contains the server password. If
+          <literal>null</literal>, the server doesn't require a password.
+        '';
+      };
     };
   };
 
@@ -71,10 +80,17 @@ in
       after       = [ "network-online.target" ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.syncplay}/bin/syncplay-server ${escapeShellArgs cmdArgs}";
         User = cfg.user;
         Group = cfg.group;
+        LoadCredential = lib.mkIf (cfg.passwordFile != null) "password:${cfg.passwordFile}";
       };
+
+      script = ''
+        ${lib.optionalString (cfg.passwordFile != null) ''
+          export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
+        ''}
+        exec ${pkgs.syncplay-nogui}/bin/syncplay-server ${escapeShellArgs cmdArgs}
+      '';
     };
   };
 }
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 3a3d4c80ecff4..66b85cd9d8a9f 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -30,15 +30,22 @@ let
   updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
     set -efu
 
+    # be careful not to leak secrets in the filesystem or in process listings
+
+    umask 0077
+
     # get the api key by parsing the config.xml
     while
-        ! api_key=$(${pkgs.libxml2}/bin/xmllint \
+        ! ${pkgs.libxml2}/bin/xmllint \
             --xpath 'string(configuration/gui/apikey)' \
-            ${cfg.configDir}/config.xml)
+            ${cfg.configDir}/config.xml \
+            >"$RUNTIME_DIRECTORY/api_key"
     do sleep 1; done
 
+    (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
+
     curl() {
-        ${pkgs.curl}/bin/curl -sSLk -H "X-API-Key: $api_key" \
+        ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
             --retry 1000 --retry-delay 1 --retry-all-errors \
             "$@"
     }
@@ -72,39 +79,39 @@ in {
       cert = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
-          Path to the <literal>cert.pem</literal> file, which will be copied into Syncthing's
-          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        description = mdDoc ''
+          Path to the `cert.pem` file, which will be copied into Syncthing's
+          [configDir](#opt-services.syncthing.configDir).
         '';
       };
 
       key = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = ''
-          Path to the <literal>key.pem</literal> file, which will be copied into Syncthing's
-          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        description = mdDoc ''
+          Path to the `key.pem` file, which will be copied into Syncthing's
+          [configDir](#opt-services.syncthing.configDir).
         '';
       };
 
       overrideDevices = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = mdDoc ''
           Whether to delete the devices which are not configured via the
-          <link linkend="opt-services.syncthing.devices">devices</link> option.
-          If set to <literal>false</literal>, devices added via the web
+          [devices](#opt-services.syncthing.devices) option.
+          If set to `false`, devices added via the web
           interface will persist and will have to be deleted manually.
         '';
       };
 
       devices = mkOption {
         default = {};
-        description = ''
+        description = mdDoc ''
           Peers/devices which Syncthing should communicate with.
 
           Note that you can still add devices manually, but those changes
-          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices)
           is enabled.
         '';
         example = {
@@ -135,27 +142,27 @@ in {
 
             id = mkOption {
               type = types.str;
-              description = ''
-                The device ID. See <link xlink:href="https://docs.syncthing.net/dev/device-ids.html"/>.
+              description = mdDoc ''
+                The device ID. See <https://docs.syncthing.net/dev/device-ids.html>.
               '';
             };
 
             introducer = mkOption {
               type = types.bool;
               default = false;
-              description = ''
+              description = mdDoc ''
                 Whether the device should act as an introducer and be allowed
                 to add folders on this computer.
-                See <link xlink:href="https://docs.syncthing.net/users/introducer.html"/>.
+                See <https://docs.syncthing.net/users/introducer.html>.
               '';
             };
 
             autoAcceptFolders = mkOption {
               type = types.bool;
               default = false;
-              description = ''
+              description = mdDoc ''
                 Automatically create or share folders that this device advertises at the default path.
-                See <link xlink:href="https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format"/>.
+                See <https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format>.
               '';
             };
 
@@ -166,21 +173,21 @@ in {
       overrideFolders = mkOption {
         type = types.bool;
         default = true;
-        description = ''
+        description = mdDoc ''
           Whether to delete the folders which are not configured via the
-          <link linkend="opt-services.syncthing.folders">folders</link> option.
-          If set to <literal>false</literal>, folders added via the web
+          [folders](#opt-services.syncthing.folders) option.
+          If set to `false`, folders added via the web
           interface will persist and will have to be deleted manually.
         '';
       };
 
       folders = mkOption {
         default = {};
-        description = ''
+        description = mdDoc ''
           Folders which should be shared by Syncthing.
 
-          Note that you can still add devices manually, but those changes
-          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          Note that you can still add folders manually, but those changes
+          will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders)
           is enabled.
         '';
         example = literalExpression ''
@@ -231,18 +238,18 @@ in {
             devices = mkOption {
               type = types.listOf types.str;
               default = [];
-              description = ''
+              description = mdDoc ''
                 The devices this folder should be shared with. Each device must
-                be defined in the <link linkend="opt-services.syncthing.devices">devices</link> option.
+                be defined in the [devices](#opt-services.syncthing.devices) option.
               '';
             };
 
             versioning = mkOption {
               default = null;
-              description = ''
+              description = mdDoc ''
                 How to keep changed/deleted files with Syncthing.
                 There are 4 different types of versioning with different parameters.
-                See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                See <https://docs.syncthing.net/users/versioning.html>.
               '';
               example = literalExpression ''
                 [
@@ -284,17 +291,17 @@ in {
                 options = {
                   type = mkOption {
                     type = enum [ "external" "simple" "staggered" "trashcan" ];
-                    description = ''
+                    description = mdDoc ''
                       The type of versioning.
-                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                      See <https://docs.syncthing.net/users/versioning.html>.
                     '';
                   };
                   params = mkOption {
                     type = attrsOf (either str path);
-                    description = ''
+                    description = mdDoc ''
                       The parameters for versioning. Structure depends on
-                      <link linkend="opt-services.syncthing.folders._name_.versioning.type">versioning.type</link>.
-                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                      [versioning.type](#opt-services.syncthing.folders._name_.versioning.type).
+                      See <https://docs.syncthing.net/users/versioning.html>.
                     '';
                   };
                 };
@@ -345,9 +352,9 @@ in {
             ignoreDelete = mkOption {
               type = types.bool;
               default = false;
-              description = ''
+              description = mdDoc ''
                 Whether to skip deleting files that are deleted by peers.
-                See <link xlink:href="https://docs.syncthing.net/advanced/folder-ignoredelete.html"/>.
+                See <https://docs.syncthing.net/advanced/folder-ignoredelete.html>.
               '';
             };
           };
@@ -357,9 +364,9 @@ in {
       extraOptions = mkOption {
         type = types.addCheck (pkgs.formats.json {}).type isAttrs;
         default = {};
-        description = ''
+        description = mdDoc ''
           Extra configuration options for Syncthing.
-          See <link xlink:href="https://docs.syncthing.net/users/config.html"/>.
+          See <https://docs.syncthing.net/users/config.html>.
         '';
         example = {
           options.localAnnounceEnabled = false;
@@ -387,9 +394,9 @@ in {
         type = types.str;
         default = defaultUser;
         example = "yourUser";
-        description = ''
+        description = mdDoc ''
           The user to run Syncthing as.
-          By default, a user named <literal>${defaultUser}</literal> will be created.
+          By default, a user named `${defaultUser}` will be created.
         '';
       };
 
@@ -397,9 +404,9 @@ in {
         type = types.str;
         default = defaultGroup;
         example = "yourGroup";
-        description = ''
+        description = mdDoc ''
           The group to run Syncthing under.
-          By default, a group named <literal>${defaultGroup}</literal> will be created.
+          By default, a group named `${defaultGroup}` will be created.
         '';
       };
 
@@ -407,11 +414,11 @@ in {
         type = with types; nullOr str;
         default = null;
         example = "socks5://address.com:1234";
-        description = ''
+        description = mdDoc ''
           Overwrites the all_proxy environment variable for the Syncthing process to
           the given value. This is normally used to let Syncthing connect
           through a SOCKS5 proxy server.
-          See <link xlink:href="https://docs.syncthing.net/users/proxying.html"/>.
+          See <https://docs.syncthing.net/users/proxying.html>.
         '';
       };
 
@@ -432,25 +439,13 @@ in {
           The path where the settings and keys will exist.
         '';
         default = cfg.dataDir + optionalString cond "/.config/syncthing";
-        defaultText = literalDocBook ''
-          <variablelist>
-            <varlistentry>
-              <term><literal>stateVersion >= 19.03</literal></term>
-              <listitem>
-                <programlisting>
-                  config.${opt.dataDir} + "/.config/syncthing"
-                </programlisting>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>otherwise</term>
-              <listitem>
-                <programlisting>
-                  config.${opt.dataDir}
-                </programlisting>
-              </listitem>
-            </varlistentry>
-          </variablelist>
+        defaultText = literalMD ''
+          * if `stateVersion >= 19.03`:
+
+                config.${opt.dataDir} + "/.config/syncthing"
+          * otherwise:
+
+                config.${opt.dataDir}
         '';
       };
 
@@ -588,6 +583,7 @@ in {
         serviceConfig = {
           User = cfg.user;
           RemainAfterExit = true;
+          RuntimeDirectory = "syncthing-init";
           Type = "oneshot";
           ExecStart = updateConfig;
         };
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 3f41646bf01e9..39c9c6fc5b0d1 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -2,9 +2,14 @@
 
 with lib;
 
-let cfg = config.services.tailscale;
+let
+  cfg = config.services.tailscale;
+  firewallOn = config.networking.firewall.enable;
+  rpfMode = config.networking.firewall.checkReversePath;
+  isNetworkd = config.networking.useNetworkd;
+  rpfIsStrict = rpfMode == true || rpfMode == "strict";
 in {
-  meta.maintainers = with maintainers; [ danderson mbaillie ];
+  meta.maintainers = with maintainers; [ danderson mbaillie twitchyliquid64 ];
 
   options.services.tailscale = {
     enable = mkEnableOption "Tailscale client daemon";
@@ -21,6 +26,12 @@ in {
       description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.'';
     };
 
+    permitCertUid = mkOption {
+      type = types.nullOr types.nonEmptyStr;
+      default = null;
+      description = "Username or user ID of the user allowed to to fetch Tailscale TLS certificates for the node.";
+    };
+
     package = mkOption {
       type = types.package;
       default = pkgs.tailscale;
@@ -30,15 +41,46 @@ in {
   };
 
   config = mkIf cfg.enable {
+    warnings = optional (firewallOn && rpfIsStrict) "Strict reverse path filtering breaks Tailscale exit node use and some subnet routing setups. Consider setting `networking.firewall.checkReversePath` = 'loose'";
     environment.systemPackages = [ cfg.package ]; # for the CLI
     systemd.packages = [ cfg.package ];
     systemd.services.tailscaled = {
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.openresolv pkgs.procps ];
+      path = [
+        config.networking.resolvconf.package # for configuring DNS in some configs
+        pkgs.procps     # for collecting running services (opt-in feature)
+        pkgs.glibc      # for `getent` to look up user shells
+      ];
       serviceConfig.Environment = [
         "PORT=${toString cfg.port}"
         ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"''
-      ];
+      ] ++ (lib.optionals (cfg.permitCertUid != null) [
+        "TS_PERMIT_CERT_UID=${cfg.permitCertUid}"
+      ]);
+      # Restart tailscaled with a single `systemctl restart` at the
+      # end of activation, rather than a `stop` followed by a later
+      # `start`. Activation over Tailscale can hang for tens of
+      # seconds in the stop+start setup, if the activation script has
+      # a significant delay between the stop and start phases
+      # (e.g. script blocked on another unit with a slow shutdown).
+      #
+      # Tailscale is aware of the correctness tradeoff involved, and
+      # already makes its upstream systemd unit robust against unit
+      # version mismatches on restart for compatibility with other
+      # linux distros.
+      stopIfChanged = false;
+    };
+
+    networking.dhcpcd.denyInterfaces = [ cfg.interfaceName ];
+
+    systemd.network.networks."50-tailscale" = mkIf isNetworkd {
+      matchConfig = {
+        Name = cfg.interfaceName;
+      };
+      linkConfig = {
+        Unmanaged = true;
+        ActivationPolicy = "manual";
+      };
     };
   };
 }
diff --git a/nixos/modules/services/networking/tetrd.nix b/nixos/modules/services/networking/tetrd.nix
index ead73c497764d..0801ce1292464 100644
--- a/nixos/modules/services/networking/tetrd.nix
+++ b/nixos/modules/services/networking/tetrd.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 {
-  options.services.tetrd.enable = lib.mkEnableOption pkgs.tetrd.meta.description;
+  options.services.tetrd.enable = lib.mkEnableOption "tetrd";
 
   config = lib.mkIf config.services.tetrd.enable {
     environment = {
diff --git a/nixos/modules/services/networking/tox-node.nix b/nixos/modules/services/networking/tox-node.nix
index c24e7fd128508..c6e5c2d6e8190 100644
--- a/nixos/modules/services/networking/tox-node.nix
+++ b/nixos/modules/services/networking/tox-node.nix
@@ -8,12 +8,7 @@ let
   homeDir = "/var/lib/tox-node";
 
   configFile = let
-    # fetchurl should be switched to getting this file from tox-node.src once
-    # the dpkg directory is in a release
-    src = pkgs.fetchurl {
-      url = "https://raw.githubusercontent.com/tox-rs/tox-node/master/dpkg/config.yml";
-      sha256 = "1431wzpzm786mcvyzk1rp7ar418n45dr75hdggxvlm7pkpam31xa";
-    };
+    src = "${pkg.src}/dpkg/config.yml";
     confJSON = pkgs.writeText "config.json" (
       builtins.toJSON {
         log-type = cfg.logType;
diff --git a/nixos/modules/services/networking/trickster.nix b/nixos/modules/services/networking/trickster.nix
index e48bba8fa587f..ac260a14d9a2d 100644
--- a/nixos/modules/services/networking/trickster.nix
+++ b/nixos/modules/services/networking/trickster.nix
@@ -6,6 +6,9 @@ let
   cfg = config.services.trickster;
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "trickster" "origin" ] [ "services" "trickster" "origin-url" ])
+  ];
 
   options = {
     services.trickster = {
@@ -58,11 +61,19 @@ in
         '';
       };
 
-      origin = mkOption {
+      origin-type = mkOption {
+        type = types.enum [ "prometheus" "influxdb" ];
+        default = "prometheus";
+        description = ''
+          Type of origin (prometheus, influxdb)
+        '';
+      };
+
+      origin-url = mkOption {
         type = types.str;
         default = "http://prometheus:9090";
         description = ''
-          URL to the Prometheus Origin. Enter it like you would in grafana, e.g., http://prometheus:9090 (default http://prometheus:9090).
+          URL to the Origin. Enter it like you would in grafana, e.g., http://prometheus:9090 (default http://prometheus:9090).
         '';
       };
 
@@ -87,7 +98,7 @@ in
 
   config = mkIf cfg.enable {
     systemd.services.trickster = {
-      description = "Dashboard Accelerator for Prometheus";
+      description = "Reverse proxy cache and time series dashboard accelerator";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
@@ -96,7 +107,8 @@ in
           ${cfg.package}/bin/trickster \
           -log-level ${cfg.log-level} \
           -metrics-port ${toString cfg.metrics-port} \
-          -origin ${cfg.origin} \
+          -origin-type ${cfg.origin-type} \
+          -origin-url ${cfg.origin-url} \
           -proxy-port ${toString cfg.proxy-port} \
           ${optionalString (cfg.configFile != null) "-config ${cfg.configFile}"} \
           ${optionalString (cfg.profiler-port != null) "-profiler-port ${cfg.profiler-port}"} \
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index f6e9634909241..87873c8c1e83b 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -62,6 +62,7 @@ in {
       };
 
       stateDir = mkOption {
+        type = types.path;
         default = "/var/lib/unbound";
         description = "Directory holding all state for unbound to run.";
       };
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index a683c537f05b2..e88daae1fbbac 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -51,7 +51,7 @@ in
 
     services.unifi.openFirewall = mkOption {
       type = types.bool;
-      default = true;
+      default = false;
       description = ''
         Whether or not to open the minimum required ports on the firewall.
 
@@ -85,10 +85,6 @@ in
 
   config = mkIf cfg.enable {
 
-    warnings = optional
-      (options.services.unifi.openFirewall.highestPrio >= (mkOptionDefault null).priority)
-      "The current services.unifi.openFirewall = true default is deprecated and will change to false in 22.11. Set it explicitly to silence this warning.";
-
     users.users.unifi = {
       isSystemUser = true;
       group = "unifi";
diff --git a/nixos/modules/services/networking/uptermd.nix b/nixos/modules/services/networking/uptermd.nix
new file mode 100644
index 0000000000000..b845a00649eba
--- /dev/null
+++ b/nixos/modules/services/networking/uptermd.nix
@@ -0,0 +1,109 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.uptermd;
+in
+{
+  options = {
+    services.uptermd = {
+      enable = mkEnableOption "uptermd";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the firewall for the port in <option>services.uptermd.port</option>.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 2222;
+        description = ''
+          Port the server will listen on.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "[::]";
+        example = "127.0.0.1";
+        description = ''
+          Address the server will listen on.
+        '';
+      };
+
+      hostKey = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/upterm_host_ed25519_key";
+        description = ''
+          Path to SSH host key. If not defined, an ed25519 keypair is generated automatically.
+        '';
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--debug" ];
+        description = ''
+          Extra flags passed to the uptermd command.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    systemd.services.uptermd = {
+      description = "Upterm Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = [ pkgs.openssh ];
+
+      preStart = mkIf (cfg.hostKey == null) ''
+        if ! [ -f ssh_host_ed25519_key ]; then
+          ssh-keygen \
+            -t ed25519 \
+            -f ssh_host_ed25519_key \
+            -N ""
+        fi
+      '';
+
+      serviceConfig = {
+        StateDirectory = "uptermd";
+        WorkingDirectory = "/var/lib/uptermd";
+        ExecStart = "${pkgs.upterm}/bin/uptermd --ssh-addr ${cfg.listenAddress}:${toString cfg.port} --private-key ${if cfg.hostKey == null then "ssh_host_ed25519_key" else cfg.hostKey} ${concatStringsSep " " cfg.extraFlags}";
+
+        # Hardening
+        AmbientCapabilities = mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        PrivateUsers = cfg.port >= 1024;
+        DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        # AF_UNIX is for ssh-keygen, which relies on nscd to resolve the uid to a user
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index 710c2d9ca17b6..d205302051e14 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -153,6 +153,7 @@ in
 
       userlist = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = "See <option>userlistFile</option>.";
       };
 
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index 414775fc35774..d44fad4202bb6 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -10,6 +10,18 @@ let
 
   interfaceOpts = { ... }: {
     options = {
+
+      configFile = mkOption {
+        example = "/secret/wg0.conf";
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+          wg-quick .conf file, describing the interface.
+          This overrides any other configuration interface configuration options.
+          See wg-quick manpage for more details.
+        '';
+      };
+
       address = mkOption {
         example = [ "192.168.2.1/24" ];
         default = [];
@@ -17,6 +29,13 @@ let
         description = "The IP addresses of the interface.";
       };
 
+      autostart = mkOption {
+        description = "Whether to bring up this interface automatically during boot.";
+        default = true;
+        example = false;
+        type = types.bool;
+      };
+
       dns = mkOption {
         example = [ "192.168.2.2" ];
         default = [];
@@ -198,13 +217,13 @@ let
   writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}");
 
   generateUnit = name: values:
-    assert assertMsg ((values.privateKey != null) != (values.privateKeyFile != null)) "Only one of privateKey or privateKeyFile may be set";
+    assert assertMsg (values.configFile != null || ((values.privateKey != null) != (values.privateKeyFile != null))) "Only one of privateKey, configFile or privateKeyFile may be set";
     let
       preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null;
       postUp =
             optional (values.privateKeyFile != null) "wg set ${name} private-key <(cat ${values.privateKeyFile})" ++
             (concatMap (peer: optional (peer.presharedKeyFile != null) "wg set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++
-            optional (values.postUp != null) values.postUp;
+            optional (values.postUp != "") values.postUp;
       postUpFile = if postUp != [] then writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) else null;
       preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null;
       postDownFile = if values.postDown != "" then writeScriptFile "postDown.sh" values.postDown else null;
@@ -240,16 +259,21 @@ let
           optionalString (peer.allowedIPs != []) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n"
         ) values.peers;
       };
-      configPath = "${configDir}/${name}.conf";
+      configPath =
+        if values.configFile != null then
+          # This uses bind-mounted private tmp folder (/tmp/systemd-private-***)
+          "/tmp/${name}.conf"
+        else
+          "${configDir}/${name}.conf";
     in
     nameValuePair "wg-quick-${name}"
       {
         description = "wg-quick WireGuard Tunnel - ${name}";
         requires = [ "network-online.target" ];
         after = [ "network.target" "network-online.target" ];
-        wantedBy = [ "multi-user.target" ];
+        wantedBy = optional values.autostart "multi-user.target";
         environment.DEVICE = name;
-        path = [ pkgs.kmod pkgs.wireguard-tools ];
+        path = [ pkgs.kmod pkgs.wireguard-tools config.networking.resolvconf.package ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -258,9 +282,17 @@ let
 
         script = ''
           ${optionalString (!config.boot.isContainer) "modprobe wireguard"}
+          ${optionalString (values.configFile != null) ''
+            cp ${values.configFile} ${configPath}
+          ''}
           wg-quick up ${configPath}
         '';
 
+        serviceConfig = {
+          # Used to privately store renamed copies of external config files during activation
+          PrivateTmp = true;
+        };
+
         preStop = ''
           wg-quick down ${configPath}
         '';
@@ -300,5 +332,11 @@ in {
     # breaks the wg-quick routing because wireguard packets leave with a fwmark from wireguard.
     networking.firewall.checkReversePath = false;
     systemd.services = mapAttrs' generateUnit cfg.interfaces;
+
+    # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend).
+    systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false;
+
+    # WireGuard interfaces should be ignored in determining whether the network is online.
+    systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces;
   };
 }
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 7cd44b2f8a0a8..a3c3c245f1d7f 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -301,8 +301,9 @@ let
       {
         description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}";
         requires = [ "wireguard-${interfaceName}.service" ];
-        after = [ "wireguard-${interfaceName}.service" ];
-        wantedBy = [ "multi-user.target" "wireguard-${interfaceName}.service" ];
+        wants = [ "network-online.target" ];
+        after = [ "wireguard-${interfaceName}.service" "network-online.target" ];
+        wantedBy = [ "wireguard-${interfaceName}.service" ];
         environment.DEVICE = interfaceName;
         environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
         path = with pkgs; [ iproute2 wireguard-tools ];
@@ -379,8 +380,9 @@ let
     nameValuePair "wireguard-${name}"
       {
         description = "WireGuard Tunnel - ${name}";
-        requires = [ "network-online.target" ];
-        after = [ "network.target" "network-online.target" ];
+        after = [ "network-pre.target" ];
+        wants = [ "network.target" ];
+        before = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
         environment.DEVICE = name;
         path = with pkgs; [ kmod iproute2 wireguard-tools ];
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index c2e1d37e28bf4..5a7975ae17828 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -114,7 +114,7 @@ let
 
       script =
       ''
-        ${optionalString configIsGenerated ''
+        ${optionalString (configIsGenerated && !cfg.allowAuxiliaryImperativeNetworks) ''
           if [ -f /etc/wpa_supplicant.conf ]; then
             echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
           fi
diff --git a/nixos/modules/services/networking/xl2tpd.nix b/nixos/modules/services/networking/xl2tpd.nix
index 7dbe51422d964..9418488c1e94f 100644
--- a/nixos/modules/services/networking/xl2tpd.nix
+++ b/nixos/modules/services/networking/xl2tpd.nix
@@ -116,18 +116,18 @@ with lib;
         #username	xl2tpd	password	*
         EOF
 
-        chown root.root ppp/chap-secrets
+        chown root:root ppp/chap-secrets
         chmod 600 ppp/chap-secrets
 
         # The documentation says this file should be present but doesn't explain why and things work even if not there:
         [ -f l2tp-secrets ] || (echo -n "* * "; ${pkgs.apg}/bin/apg -n 1 -m 32 -x 32 -a 1 -M LCN) > l2tp-secrets
-        chown root.root l2tp-secrets
+        chown root:root l2tp-secrets
         chmod 600 l2tp-secrets
 
         popd > /dev/null
 
         mkdir -p /run/xl2tpd
-        chown root.root /run/xl2tpd
+        chown root:root /run/xl2tpd
         chmod 700       /run/xl2tpd
       '';
 
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index 99c18ae6919ea..ae681988ea119 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -61,10 +61,10 @@ in {
       };
 
       group = mkOption {
-        type = types.str;
-        default = "root";
+        type = types.nullOr types.str;
+        default = null;
         example = "wheel";
-        description = "Group to grant access to the Yggdrasil control socket.";
+        description = "Group to grant access to the Yggdrasil control socket. If <code>null</code>, only root can access the socket.";
       };
 
       openMulticastPort = mkOption {
@@ -154,27 +154,16 @@ in {
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "always";
 
-        Group = cfg.group;
+        DynamicUser = true;
+        StateDirectory = "yggdrasil";
         RuntimeDirectory = "yggdrasil";
         RuntimeDirectoryMode = "0750";
         BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
           ++ lib.optional cfg.persistentKeys keysPath;
+        ReadWritePaths = "/run/yggdrasil";
 
-        # TODO: as of yggdrasil 0.3.8 and systemd 243, yggdrasil fails
-        # to set up the network adapter when DynamicUser is set.  See
-        # github.com/yggdrasil-network/yggdrasil-go/issues/557.  The
-        # following options are implied by DynamicUser according to
-        # the systemd.exec documentation, and can be removed if the
-        # upstream issue is fixed and DynamicUser is set to true:
-        PrivateTmp = true;
-        RemoveIPC = true;
-        NoNewPrivileges = true;
-        ProtectSystem = "strict";
-        RestrictSUIDSGID = true;
-        # End of list of options implied by DynamicUser.
-
-        AmbientCapabilities = "CAP_NET_ADMIN";
-        CapabilityBoundingSet = "CAP_NET_ADMIN";
+        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
         MemoryDenyWriteExecute = true;
         ProtectControlGroups = true;
         ProtectHome = "tmpfs";
@@ -185,7 +174,9 @@ in {
         RestrictRealtime = true;
         SystemCallArchitectures = "native";
         SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @resources";
-      };
+      } // (if (cfg.group != null) then {
+        Group = cfg.group;
+      } else {});
     };
 
     networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
diff --git a/nixos/modules/services/networking/zeronet.nix b/nixos/modules/services/networking/zeronet.nix
index 3370390a4c626..8be6692561d28 100644
--- a/nixos/modules/services/networking/zeronet.nix
+++ b/nixos/modules/services/networking/zeronet.nix
@@ -19,6 +19,13 @@ in with lib; {
   options.services.zeronet = {
     enable = mkEnableOption "zeronet";
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.zeronet;
+      defaultText = literalExpression "pkgs.zeronet";
+      description = "ZeroNet package to use";
+    };
+
     settings = mkOption {
       type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
       default = {};
@@ -72,7 +79,7 @@ in with lib; {
 
     systemd.services.zeronet = {
       description = "zeronet";
-      after = [ "network.target" (optionalString cfg.tor "tor.service") ];
+      after = [ "network.target" ] ++ optional cfg.tor "tor.service";
       wantedBy = [ "multi-user.target" ];
 
       serviceConfig = {
@@ -80,7 +87,7 @@ in with lib; {
         DynamicUser = true;
         StateDirectory = "zeronet";
         SupplementaryGroups = mkIf cfg.tor [ "tor" ];
-        ExecStart = "${pkgs.zeronet}/bin/zeronet --config_file ${configFile}";
+        ExecStart = "${cfg.package}/bin/zeronet --config_file ${configFile}";
       };
     };
   };
@@ -90,5 +97,5 @@ in with lib; {
     (mkRemovedOptionModule [ "services" "zeronet" "logDir" ] "Zeronet will log by default in /var/lib/zeronet")
   ];
 
-  meta.maintainers = with maintainers; [ chiiruno ];
+  meta.maintainers = with maintainers; [ Madouura ];
 }
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
index 340cbbf02fb40..95a0ad8770e22 100644
--- a/nixos/modules/services/security/clamav.nix
+++ b/nixos/modules/services/security/clamav.nix
@@ -9,7 +9,7 @@ let
   pkg = pkgs.clamav;
 
   toKeyValue = generators.toKeyValue {
-    mkKeyValue = generators.mkKeyValueDefault {} " ";
+    mkKeyValue = generators.mkKeyValueDefault { } " ";
     listsAsDuplicateKeys = true;
   };
 
@@ -30,7 +30,7 @@ in
 
         settings = mkOption {
           type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
-          default = {};
+          default = { };
           description = ''
             ClamAV configuration. Refer to <link xlink:href="https://linux.die.net/man/5/clamd.conf"/>,
             for details on supported values.
@@ -59,7 +59,7 @@ in
 
         settings = mkOption {
           type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
-          default = {};
+          default = { };
           description = ''
             freshclam configuration. Refer to <link xlink:href="https://linux.die.net/man/5/freshclam.conf"/>,
             for details on supported values.
@@ -104,7 +104,6 @@ in
     systemd.services.clamav-daemon = mkIf cfg.daemon.enable {
       description = "ClamAV daemon (clamd)";
       after = optional cfg.updater.enable "clamav-freshclam.service";
-      requires = optional cfg.updater.enable "clamav-freshclam.service";
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ clamdConfigFile ];
 
@@ -134,7 +133,7 @@ in
     systemd.services.clamav-freshclam = mkIf cfg.updater.enable {
       description = "ClamAV virus database updater (freshclam)";
       restartTriggers = [ freshclamConfigFile ];
-
+      after = [ "network-online.target" ];
       preStart = ''
         mkdir -m 0755 -p ${stateDir}
         chown ${clamavUser}:${clamavGroup} ${stateDir}
diff --git a/nixos/modules/services/security/fprot.nix b/nixos/modules/services/security/fprot.nix
deleted file mode 100644
index df60d553e85bd..0000000000000
--- a/nixos/modules/services/security/fprot.nix
+++ /dev/null
@@ -1,82 +0,0 @@
-{ config, lib, pkgs, ... }:
-with lib;
-let
-  fprotUser = "fprot";
-  stateDir = "/var/lib/fprot";
-  fprotGroup = fprotUser;
-  cfg = config.services.fprot;
-in {
-  options = {
-
-    services.fprot = {
-      updater = {
-        enable = mkEnableOption "automatic F-Prot virus definitions database updates";
-
-        productData = mkOption {
-          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.
-          '';
-        };
-
-      };
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.updater.enable {
-
-    services.fprot.updater.productData = mkDefault "${pkgs.fprot}/opt/f-prot/product.data";
-    services.fprot.updater.licenseKeyfile = mkDefault "${pkgs.fprot}/opt/f-prot/license.key";
-
-    environment.systemPackages = [ pkgs.fprot ];
-    environment.etc."f-prot.conf" = {
-      source = "${pkgs.fprot}/opt/f-prot/f-prot.conf";
-    };
-
-    users.users.${fprotUser} =
-      { uid = config.ids.uids.fprot;
-        description = "F-Prot daemon user";
-        home = stateDir;
-      };
-
-    users.groups.${fprotGroup} =
-      { gid = config.ids.gids.fprot; };
-
-    services.cron.systemCronJobs = [ "*/${toString cfg.updater.frequency} * * * * root start fprot-updater" ];
-
-    systemd.services.fprot-updater = {
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = false;
-      };
-      wantedBy = [ "multi-user.target" ];
-
-      # have to copy fpupdate executable because it insists on storing the virus database in the same dir
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-        chown ${fprotUser}:${fprotGroup} ${stateDir}
-        cp ${pkgs.fprot}/opt/f-prot/fpupdate ${stateDir}
-        ln -sf ${cfg.updater.productData} ${stateDir}/product.data
-      '';
-
-      script = "/var/lib/fprot/fpupdate --keyfile ${cfg.updater.licenseKeyfile}";
-    };
- };
-}
diff --git a/nixos/modules/services/security/infnoise.nix b/nixos/modules/services/security/infnoise.nix
new file mode 100644
index 0000000000000..4fb8adaf33f89
--- /dev/null
+++ b/nixos/modules/services/security/infnoise.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.infnoise;
+in {
+  options = {
+    services.infnoise = {
+      enable = mkEnableOption "the Infinite Noise TRNG driver";
+
+      fillDevRandom = mkOption {
+        description = ''
+          Whether to run the infnoise driver as a daemon to refill /dev/random.
+
+          If disabled, you can use the `infnoise` command-line tool to
+          manually obtain randomness.
+        '';
+        type = types.bool;
+        default = true;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.infnoise ];
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", SYMLINK+="infnoise", TAG+="systemd", GROUP="dialout", MODE="0664", ENV{SYSTEMD_WANTS}="infnoise.service"
+    '';
+
+    systemd.services.infnoise = mkIf cfg.fillDevRandom {
+      description = "Infinite Noise TRNG driver";
+
+      bindsTo = [ "dev-infnoise.device" ];
+      after = [ "dev-infnoise.device" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.infnoise}/bin/infnoise --dev-random --debug";
+        Restart = "always";
+        User = "infnoise";
+        DynamicUser = true;
+        SupplementaryGroups = [ "dialout" ];
+        DeviceAllow = [ "/dev/infnoise" ];
+        DevicePolicy = "closed";
+        PrivateNetwork = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true; # only reads entropy pool size and watermark
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
new file mode 100644
index 0000000000000..a7c51b9a877f1
--- /dev/null
+++ b/nixos/modules/services/security/kanidm.nix
@@ -0,0 +1,345 @@
+{ config, lib, options, pkgs, ... }:
+let
+  cfg = config.services.kanidm;
+  settingsFormat = pkgs.formats.toml { };
+  # Remove null values, so we can document optional values that don't end up in the generated TOML file.
+  filterConfig = lib.converge (lib.filterAttrsRecursive (_: v: v != null));
+  serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
+  clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
+  unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
+
+  defaultServiceConfig = {
+    BindReadOnlyPaths = [
+      "/nix/store"
+      "-/etc/resolv.conf"
+      "-/etc/nsswitch.conf"
+      "-/etc/hosts"
+      "-/etc/localtime"
+    ];
+    CapabilityBoundingSet = "";
+    # ProtectClock= adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    # Implies ProtectSystem=strict, which re-mounts all paths
+    # DynamicUser = true;
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateNetwork = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProcSubset = "pid";
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    # Would re-mount paths ignored by temporary root
+    #ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    RestrictAddressFamilies = [ ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+    # Does not work well with the temporary root
+    #UMask = "0066";
+  };
+
+in
+{
+  options.services.kanidm = {
+    enableClient = lib.mkEnableOption "the Kanidm client";
+    enableServer = lib.mkEnableOption "the Kanidm server";
+    enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration.";
+
+    serverSettings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          bindaddress = lib.mkOption {
+            description = "Address/port combination the webserver binds to.";
+            example = "[::1]:8443";
+            type = lib.types.str;
+          };
+          # Should be optional but toml does not accept null
+          ldapbindaddress = lib.mkOption {
+            description = ''
+              Address and port the LDAP server is bound to. Setting this to <literal>null</literal> disables the LDAP interface.
+            '';
+            example = "[::1]:636";
+            default = null;
+            type = lib.types.nullOr lib.types.str;
+          };
+          origin = lib.mkOption {
+            description = "The origin of your Kanidm instance. Must have https as protocol.";
+            example = "https://idm.example.org";
+            type = lib.types.strMatching "^https://.*";
+          };
+          domain = lib.mkOption {
+            description = ''
+              The <literal>domain</literal> that Kanidm manages. Must be below or equal to the domain
+              specified in <literal>serverSettings.origin</literal>.
+              This can be left at <literal>null</literal>, only if your instance has the role <literal>ReadOnlyReplica</literal>.
+              While it is possible to change the domain later on, it requires extra steps!
+              Please consider the warnings and execute the steps described
+              <link xlink:href="https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain">in the documentation</link>.
+            '';
+            example = "example.org";
+            default = null;
+            type = lib.types.nullOr lib.types.str;
+          };
+          db_path = lib.mkOption {
+            description = "Path to Kanidm database.";
+            default = "/var/lib/kanidm/kanidm.db";
+            readOnly = true;
+            type = lib.types.path;
+          };
+          log_level = lib.mkOption {
+            description = "Log level of the server.";
+            default = "default";
+            type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ];
+          };
+          role = lib.mkOption {
+            description = "The role of this server. This affects the replication relationship and thereby available features.";
+            default = "WriteReplica";
+            type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Settings for Kanidm, see
+        <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md">the documentation</link>
+        and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/server.toml">example configuration</link>
+        for possible values.
+      '';
+    };
+
+    clientSettings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.uri = lib.mkOption {
+          description = "Address of the Kanidm server.";
+          example = "http://127.0.0.1:8080";
+          type = lib.types.str;
+        };
+      };
+      description = ''
+        Configure Kanidm clients, needed for the PAM daemon. See
+        <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration">the documentation</link>
+        and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/config">example configuration</link>
+        for possible values.
+      '';
+    };
+
+    unixSettings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.pam_allowed_login_groups = lib.mkOption {
+          description = "Kanidm groups that are allowed to login using PAM.";
+          example = "my_pam_group";
+          type = lib.types.listOf lib.types.str;
+        };
+      };
+      description = ''
+        Configure Kanidm unix daemon.
+        See <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon">the documentation</link>
+        and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/unixd">example configuration</link>
+        for possible values.
+      '';
+    };
+  };
+
+  config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
+    assertions =
+      [
+        {
+          assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
+          message = ''
+            <option>services.kanidm.serverSettings.tls_chain</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+        }
+        {
+          assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
+          message = ''
+            <option>services.kanidm.serverSettings.tls_key</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+        }
+        {
+          assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
+          message = ''
+            <option>services.kanidm.clientSettings</option> needs to be configured
+            if the client is enabled.
+          '';
+        }
+        {
+          assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
+          message = ''
+            <option>services.kanidm.clientSettings</option> needs to be configured
+            for the PAM daemon to connect to the Kanidm server.
+          '';
+        }
+        {
+          assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
+            -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
+          message = ''
+            <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
+            is not a ReadOnlyReplica. Otherwise the db would inherit it from
+            the instance it follows.
+          '';
+        }
+      ];
+
+    environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ];
+
+    systemd.services.kanidm = lib.mkIf cfg.enableServer {
+      description = "kanidm identity management daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = defaultServiceConfig // {
+        StateDirectory = "kanidm";
+        StateDirectoryMode = "0700";
+        ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
+        User = "kanidm";
+        Group = "kanidm";
+
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+        # This would otherwise override the CAP_NET_BIND_SERVICE capability.
+        PrivateUsers = false;
+        # Port needs to be exposed to the host network
+        PrivateNetwork = false;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        TemporaryFileSystem = "/:ro";
+      };
+      environment.RUST_LOG = "info";
+    };
+
+    systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam {
+      description = "Kanidm PAM daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      restartTriggers = [ unixConfigFile clientConfigFile ];
+      serviceConfig = defaultServiceConfig // {
+        CacheDirectory = "kanidm-unixd";
+        CacheDirectoryMode = "0700";
+        RuntimeDirectory = "kanidm-unixd";
+        ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
+        User = "kanidm-unixd";
+        Group = "kanidm-unixd";
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+          "-/etc/kanidm"
+          "-/etc/static/kanidm"
+        ];
+        BindPaths = [
+          # To create the socket
+          "/run/kanidm-unixd:/var/run/kanidm-unixd"
+        ];
+        # Needs to connect to kanidmd
+        PrivateNetwork = false;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        TemporaryFileSystem = "/:ro";
+      };
+      environment.RUST_LOG = "info";
+    };
+
+    systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
+      description = "Kanidm PAM home management daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "kanidm-unixd.service" ];
+      partOf = [ "kanidm-unixd.service" ];
+      restartTriggers = [ unixConfigFile clientConfigFile ];
+      serviceConfig = {
+        ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks";
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+          "-/etc/kanidm"
+          "-/etc/static/kanidm"
+        ];
+        BindPaths = [
+          # To manage home directories
+          "/home"
+          # To connect to kanidm-unixd
+          "/run/kanidm-unixd:/var/run/kanidm-unixd"
+        ];
+        # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
+        CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
+        IPAddressDeny = "any";
+        # Need access to users
+        PrivateUsers = false;
+        # Need access to home directories
+        ProtectHome = false;
+        RestrictAddressFamilies = [ "AF_UNIX" ];
+        TemporaryFileSystem = "/:ro";
+      };
+      environment.RUST_LOG = "info";
+    };
+
+    # These paths are hardcoded
+    environment.etc = lib.mkMerge [
+      (lib.mkIf options.services.kanidm.clientSettings.isDefined {
+        "kanidm/config".source = clientConfigFile;
+      })
+      (lib.mkIf cfg.enablePam {
+        "kanidm/unixd".source = unixConfigFile;
+      })
+    ];
+
+    system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ];
+
+    system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
+    system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
+
+    users.groups = lib.mkMerge [
+      (lib.mkIf cfg.enableServer {
+        kanidm = { };
+      })
+      (lib.mkIf cfg.enablePam {
+        kanidm-unixd = { };
+      })
+    ];
+    users.users = lib.mkMerge [
+      (lib.mkIf cfg.enableServer {
+        kanidm = {
+          description = "Kanidm server";
+          isSystemUser = true;
+          group = "kanidm";
+          packages = with pkgs; [ kanidm ];
+        };
+      })
+      (lib.mkIf cfg.enablePam {
+        kanidm-unixd = {
+          description = "Kanidm PAM daemon";
+          isSystemUser = true;
+          group = "kanidm-unixd";
+        };
+      })
+    ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 4d35624241708..5c89d58723760 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -102,17 +102,19 @@ in
     # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
     provider = mkOption {
       type = types.enum [
-        "google"
+        "adfs"
         "azure"
+        "bitbucket"
+        "digitalocean"
         "facebook"
         "github"
-        "keycloak"
         "gitlab"
+        "google"
+        "keycloak"
+        "keycloak-oidc"
         "linkedin"
         "login.gov"
-        "bitbucket"
         "nextcloud"
-        "digitalocean"
         "oidc"
       ];
       default = "google";
@@ -569,8 +571,11 @@ in
     users.users.oauth2_proxy = {
       description = "OAuth2 Proxy";
       isSystemUser = true;
+      group = "oauth2_proxy";
     };
 
+    users.groups.oauth2_proxy = {};
+
     systemd.services.oauth2_proxy = {
       description = "OAuth2 Proxy";
       path = [ cfg.package ];
diff --git a/nixos/modules/services/security/opensnitch.nix b/nixos/modules/services/security/opensnitch.nix
index 919346cf2bb11..f9b4985e19913 100644
--- a/nixos/modules/services/security/opensnitch.nix
+++ b/nixos/modules/services/security/opensnitch.nix
@@ -3,22 +3,123 @@
 with lib;
 
 let
-  name = "opensnitch";
   cfg = config.services.opensnitch;
+  format = pkgs.formats.json {};
 in {
   options = {
     services.opensnitch = {
       enable = mkEnableOption "Opensnitch application firewall";
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+
+          options = {
+            Server = {
+
+              Address = mkOption {
+                type = types.str;
+                description = ''
+                  Unix socket path (unix:///tmp/osui.sock, the "unix:///" part is
+                  mandatory) or TCP socket (192.168.1.100:50051).
+                '';
+              };
+
+              LogFile = mkOption {
+                type = types.path;
+                description = ''
+                  File to write logs to (use /dev/stdout to write logs to standard
+                  output).
+                '';
+              };
+
+            };
+
+            DefaultAction = mkOption {
+              type = types.enum [ "allow" "deny" ];
+              description = ''
+                Default action whether to block or allow application internet
+                access.
+              '';
+            };
+
+            DefaultDuration = mkOption {
+              type = types.enum [
+                "once" "always" "until restart" "30s" "5m" "15m" "30m" "1h"
+              ];
+              description = ''
+                Default duration of firewall rule.
+              '';
+            };
+
+            InterceptUnknown = mkOption {
+              type = types.bool;
+              description = ''
+                Wheter to intercept spare connections.
+              '';
+            };
+
+            ProcMonitorMethod = mkOption {
+              type = types.enum [ "ebpf" "proc" "ftrace" "audit" ];
+              description = ''
+                Which process monitoring method to use.
+              '';
+            };
+
+            LogLevel = mkOption {
+              type = types.enum [ 0 1 2 3 4 ];
+              description = ''
+                Default log level from 0 to 4 (debug, info, important, warning,
+                error).
+              '';
+            };
+
+            Firewall = mkOption {
+              type = types.enum [ "iptables" "nftables" ];
+              description = ''
+                Which firewall backend to use.
+              '';
+            };
+
+            Stats = {
+
+              MaxEvents = mkOption {
+                type = types.int;
+                description = ''
+                  Max events to send to the GUI.
+                '';
+              };
+
+              MaxStats = mkOption {
+                type = types.int;
+                description = ''
+                  Max stats per item to keep in backlog.
+                '';
+              };
+
+            };
+          };
+        };
+        description = ''
+          opensnitchd configuration. Refer to
+          <link xlink:href="https://github.com/evilsocket/opensnitch/wiki/Configurations"/>
+          for details on supported values.
+        '';
+      };
     };
   };
 
   config = mkIf cfg.enable {
 
+    # pkg.opensnitch is referred to elsewhere in the module so we don't need to worry about it being garbage collected
+    services.opensnitch.settings = mapAttrs (_: v: mkDefault v) (builtins.fromJSON (builtins.unsafeDiscardStringContext (builtins.readFile "${pkgs.opensnitch}/etc/default-config.json")));
+
     systemd = {
       packages = [ pkgs.opensnitch ];
       services.opensnitchd.wantedBy = [ "multi-user.target" ];
     };
 
+    environment.etc."opensnitchd/default-config.json".source = format.generate "default-config.json" cfg.settings;
+
   };
 }
 
diff --git a/nixos/modules/services/security/pass-secret-service.nix b/nixos/modules/services/security/pass-secret-service.nix
new file mode 100644
index 0000000000000..6ae190eb95e6b
--- /dev/null
+++ b/nixos/modules/services/security/pass-secret-service.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.passSecretService;
+in
+{
+  options.services.passSecretService = {
+    enable = mkEnableOption "pass secret service";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.pass-secret-service;
+      defaultText = literalExpression "pkgs.pass-secret-service";
+      description = "Which pass-secret-service package to use.";
+      example = literalExpression "pkgs.pass-secret-service.override { python3 = pkgs.python310 }";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.packages = [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+  };
+
+  meta.maintainers = with maintainers; [ aidalgol ];
+}
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index b8e2d9a8b0dfc..1f5639d475e85 100644
--- a/nixos/modules/services/security/privacyidea.nix
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.privacyidea;
   opt = options.services.privacyidea;
 
-  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python39; };
   python = uwsgi.python3;
   penv = python.withPackages (const [ pkgs.privacyidea ]);
   logCfg = pkgs.writeText "privacyidea-log.cfg" ''
@@ -51,6 +51,16 @@ let
     ${cfg.extraConfig}
   '';
 
+  renderValue = x:
+    if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
+    else if isString x && hasInfix "," x then ''"${x}"''
+    else x;
+
+  ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
+    (generators.toINI {}
+      (flip mapAttrs cfg.ldap-proxy.settings
+        (const (mapAttrs (const renderValue)))));
+
 in
 
 {
@@ -172,7 +182,8 @@ in
         enable = mkEnableOption "PrivacyIDEA LDAP Proxy";
 
         configFile = mkOption {
-          type = types.path;
+          type = types.nullOr types.path;
+          default = null;
           description = ''
             Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
           '';
@@ -189,6 +200,26 @@ in
           default = "pi-ldap-proxy";
           description = "Group account under which PrivacyIDEA LDAP proxy runs.";
         };
+
+        settings = mkOption {
+          type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
+          default = {};
+          description = ''
+            Attribute-set containing the settings for <package>privacyidea-ldap-proxy</package>.
+            It's possible to pass secrets using env-vars as substitutes and
+            use the option <xref linkend="opt-services.privacyidea.ldap-proxy.environmentFile" />
+            to inject them via <package>envsubst</package>.
+          '';
+        };
+
+        environmentFile = mkOption {
+          default = null;
+          type = types.nullOr types.str;
+          description = ''
+            Environment file containing secrets to be substituted into
+            <xref linkend="opt-services.privacyidea.ldap-proxy.settings" />.
+          '';
+        };
       };
     };
   };
@@ -276,6 +307,18 @@ in
 
     (mkIf cfg.ldap-proxy.enable {
 
+      assertions = [
+        { assertion = let
+            xor = a: b: a && !b || !a && b;
+          in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
+          message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
+        }
+      ];
+
+      warnings = mkIf (cfg.ldap-proxy.configFile != null) [
+        "Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
+      ];
+
       systemd.services.privacyidea-ldap-proxy = let
         ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
       in {
@@ -284,14 +327,28 @@ in
         serviceConfig = {
           User = cfg.ldap-proxy.user;
           Group = cfg.ldap-proxy.group;
-          ExecStart = ''
+          StateDirectory = "privacyidea-ldap-proxy";
+          EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
+            [ cfg.ldap-proxy.environmentFile ];
+          ExecStartPre =
+            "${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
+              umask 0077
+              ${pkgs.envsubst}/bin/envsubst \
+                -i ${ldapProxyConfig} \
+                -o $STATE_DIRECTORY/ldap-proxy.ini
+            ''}";
+          ExecStart = let
+            configPath = if cfg.ldap-proxy.settings != {}
+              then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
+              else cfg.ldap-proxy.configFile;
+          in ''
             ${ldap-proxy-env}/bin/twistd \
               --nodaemon \
               --pidfile= \
               -u ${cfg.ldap-proxy.user} \
               -g ${cfg.ldap-proxy.group} \
               ldap-proxy \
-              -c ${cfg.ldap-proxy.configFile}
+              -c ${configPath}
           '';
           Restart = "always";
         };
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index 53bd9efa5ac7b..3be0a8c700b9e 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -17,7 +17,7 @@ let
       else "sshg-fw-ipset";
   in pkgs.writeText "sshguard.conf" ''
     BACKEND="${pkgs.sshguard}/libexec/${backend}"
-    LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
+    LOGREADER="LANG=C ${config.systemd.package}/bin/journalctl ${args}"
   '';
 
 in {
diff --git a/nixos/modules/services/security/sslmate-agent.nix b/nixos/modules/services/security/sslmate-agent.nix
new file mode 100644
index 0000000000000..c850eb22a0311
--- /dev/null
+++ b/nixos/modules/services/security/sslmate-agent.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sslmate-agent;
+
+in {
+  meta.maintainers = with maintainers; [ wolfangaukang ];
+
+  options = {
+    services.sslmate-agent = {
+      enable = mkEnableOption "sslmate-agent, a daemon for managing SSL/TLS certificates on a server";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ sslmate-agent ];
+
+    systemd = {
+      packages = [ pkgs.sslmate-agent ];
+      services.sslmate-agent = {
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ConfigurationDirectory = "sslmate-agent";
+          LogsDirectory = "sslmate-agent";
+          StateDirectory = "sslmate-agent";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
index 27b2ceed1a430..95183078d7b64 100644
--- a/nixos/modules/services/security/step-ca.nix
+++ b/nixos/modules/services/security/step-ca.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, nixosTests, ... }:
+{ config, lib, pkgs, ... }:
 let
   cfg = config.services.step-ca;
   settingsFormat = (pkgs.formats.json { });
@@ -82,8 +82,6 @@ in
       });
     in
     {
-      passthru.tests.step-ca = nixosTests.step-ca;
-
       assertions =
         [
           {
@@ -108,6 +106,9 @@ in
           ConditionFileNotEmpty = ""; # override upstream
         };
         serviceConfig = {
+          User = "step-ca";
+          Group = "step-ca";
+          UMask = "0077";
           Environment = "HOME=%S/step-ca";
           WorkingDirectory = ""; # override upstream
           ReadWriteDirectories = ""; # override upstream
@@ -129,6 +130,14 @@ in
         };
       };
 
+      users.users.step-ca = {
+        home = "/var/lib/step-ca";
+        group = "step-ca";
+        isSystemUser = true;
+      };
+
+      users.groups.step-ca = {};
+
       networking.firewall = lib.mkIf cfg.openFirewall {
         allowedTCPPorts = [ cfg.port ];
       };
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index cafb44e124298..a5822c02794d9 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -910,6 +910,11 @@ in
         ORPort = mkForce [];
         PublishServerDescriptor = mkForce false;
       })
+      (mkIf (!cfg.client.enable) {
+        # Make sure application connections via SOCKS are disabled
+        # when services.tor.client.enable is false
+        SOCKSPort = mkForce [ 0 ];
+      })
       (mkIf cfg.client.enable (
         { SOCKSPort = [ cfg.client.socksListenAddress ];
         } // optionalAttrs cfg.client.transparentProxy.enable {
@@ -962,7 +967,7 @@ in
               '') onion.authorizedClients ++
               optional (onion.secretKey != null) ''
                 install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path}
-                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey})"
+                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey} | head -1)"
                 case "$key" in
                  ("== ed25519v"*"-secret")
                   install -o tor -g tor -m 0400 ${escapeShellArg onion.secretKey} ${escapeShellArg onion.path}/hs_ed25519_secret_key;;
@@ -1008,7 +1013,11 @@ in
         #InaccessiblePaths = [ "-+${runDir}/root" ];
         UMask = "0066";
         BindPaths = [ stateDir ];
-        BindReadOnlyPaths = [ storeDir "/etc" ];
+        BindReadOnlyPaths = [ storeDir "/etc" ] ++
+          optionals config.services.resolved.enable [
+            "/run/systemd/resolve/stub-resolv.conf"
+            "/run/systemd/resolve/resolv.conf"
+          ];
         AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
         CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
         # ProtectClock= adds DeviceAllow=char-rtc r
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index d48bc472cb826..4942a05fe73b7 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -7,6 +7,8 @@ let
   opt = options.services.vault;
 
   configFile = pkgs.writeText "vault.hcl" ''
+    # vault in dev mode will refuse to start if its configuration sets listener
+    ${lib.optionalString (!cfg.dev) ''
     listener "tcp" {
       address = "${cfg.address}"
       ${if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then ''
@@ -17,6 +19,7 @@ let
         ''}
       ${cfg.listenerExtraConfig}
     }
+    ''}
     storage "${cfg.storageBackend}" {
       ${optionalString (cfg.storagePath   != null) ''path = "${cfg.storagePath}"''}
       ${optionalString (cfg.storageConfig != null) cfg.storageConfig}
@@ -30,8 +33,10 @@ let
   '';
 
   allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
-
-  configOptions = escapeShellArgs (concatMap (p: ["-config" p]) allConfigPaths);
+  configOptions = escapeShellArgs
+    (lib.optional cfg.dev "-dev" ++
+     lib.optional (cfg.dev && cfg.devRootTokenID != null) "-dev-root-token-id=${cfg.devRootTokenID}"
+      ++ (concatMap (p: ["-config" p]) allConfigPaths));
 
 in
 
@@ -47,6 +52,22 @@ in
         description = "This option specifies the vault package to use.";
       };
 
+      dev = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          In this mode, Vault runs in-memory and starts unsealed. This option is not meant production but for development and testing i.e. for nixos tests.
+        '';
+      };
+
+      devRootTokenID = mkOption {
+        type = types.str;
+        default = false;
+        description = ''
+          Initial root token. This only applies when <option>services.vault.dev</option> is true
+        '';
+      };
+
       address = mkOption {
         type = types.str;
         default = "127.0.0.1:8200";
@@ -186,6 +207,9 @@ in
         Group = "vault";
         ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
         ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        StateDirectory = "vault";
+        # In `dev` mode vault will put its token here
+        Environment = lib.optional (cfg.dev) "HOME=/var/lib/vault";
         PrivateDevices = true;
         PrivateTmp = true;
         ProtectSystem = "full";
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index fd459f70ccde0..756e0ee93b219 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -62,20 +62,52 @@ in {
       default = {};
       example = literalExpression ''
         {
-          domain = "https://bw.domain.tld:8443";
-          signupsAllowed = true;
-          rocketPort = 8222;
-          rocketLog = "critical";
+          DOMAIN = "https://bitwarden.example.com";
+          SIGNUPS_ALLOWED = false;
+
+          # Vaultwarden currently recommends running behind a reverse proxy
+          # (nginx or similar) for TLS termination, see
+          # https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#reverse-proxying
+          # > you should avoid enabling HTTPS via vaultwarden's built-in Rocket TLS support,
+          # > especially if your instance is publicly accessible.
+          #
+          # A suitable NixOS nginx reverse proxy example config might be:
+          #
+          #     services.nginx.virtualHosts."bitwarden.example.com" = {
+          #       enableACME = true;
+          #       forceSSL = true;
+          #       locations."/" = {
+          #         proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
+          #       };
+          #     };
+          ROCKET_ADDRESS = "127.0.0.1";
+          ROCKET_PORT = 8222;
+
+          ROCKET_LOG = "critical";
+
+          # This example assumes a mailserver running on localhost,
+          # thus without transport encryption.
+          # If you use an external mail server, follow:
+          #   https://github.com/dani-garcia/vaultwarden/wiki/SMTP-configuration
+          SMTP_HOST = "127.0.0.1";
+          SMTP_PORT = 25;
+          SMTP_SSL = false;
+
+          SMTP_FROM = "admin@bitwarden.example.com";
+          SMTP_FROM_NAME = "example.com Bitwarden server";
         }
       '';
       description = ''
         The configuration of vaultwarden is done through environment variables,
-        therefore the names are converted from camel case (e.g. disable2FARemember)
-        to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
+        therefore it is recommended to use upper snake case (e.g. <envar>DISABLE_2FA_REMEMBER</envar>).
+
+        However, camel case (e.g. <literal>disable2FARemember</literal>) is also supported:
+        The NixOS module will convert it automatically to
+        upper case snake case (e.g. <envar>DISABLE_2FA_REMEMBER</envar>).
         In this conversion digits (0-9) are handled just like upper case characters,
-        so foo2 would be converted to FOO_2.
-        Names already in this format remain unchanged, so FOO2 remains FOO2 if passed as such,
-        even though foo2 would have been converted to FOO_2.
+        so <literal>foo2</literal> would be converted to <envar>FOO_2</envar>.
+        Names already in this format remain unchanged, so <literal>FOO2</literal> remains <literal>FOO2</literal> if passed as such,
+        even though <literal>foo2</literal> would have been converted to <envar>FOO_2</envar>.
         This allows working around any potential future conflicting naming conventions.
 
         Based on the attributes passed to this config option an environment file will be generated
@@ -83,13 +115,16 @@ in {
 
         The available configuration options can be found in
         <link xlink:href="https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template">the environment template file</link>.
+
+        See <xref linkend="opt-services.vaultwarden.environmentFile" /> for how
+        to set up access to the Admin UI to invite initial users.
       '';
     };
 
     environmentFile = mkOption {
       type = with types; nullOr path;
       default = null;
-      example = "/root/vaultwarden.env";
+      example = "/var/lib/vaultwarden.env";
       description = ''
         Additional environment file as defined in <citerefentry>
         <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
@@ -100,6 +135,23 @@ in {
 
         Note that this file needs to be available on the host on which
         <literal>vaultwarden</literal> is running.
+
+        As a concrete example, to make the Admin UI available
+        (from which new users can be invited initially),
+        the secret <envar>ADMIN_TOKEN</envar> needs to be defined as described
+        <link xlink:href="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page">here</link>.
+        Setting <literal>environmentFile</literal> to <literal>/var/lib/vaultwarden.env</literal>
+        and ensuring permissions with e.g.
+        <literal>chown vaultwarden:vaultwarden /var/lib/vaultwarden.env</literal>
+        (the <literal>vaultwarden</literal> user will only exist after activating with
+        <literal>enable = true;</literal> before this), we can set the contents of the file to have
+        contents such as:
+
+<programlisting>
+# Admin secret token, see
+# https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
+ADMIN_TOKEN=...copy-paste a unique generated secret token here...
+</programlisting>
       '';
     };
 
@@ -151,7 +203,7 @@ in {
     };
 
     systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
-      aliases = [ "backup-bitwarden_rs" ];
+      aliases = [ "backup-bitwarden_rs.service" ];
       description = "Backup vaultwarden";
       environment = {
         DATA_FOLDER = "/var/lib/bitwarden_rs";
@@ -169,7 +221,7 @@ in {
     };
 
     systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
-      aliases = [ "backup-bitwarden_rs" ];
+      aliases = [ "backup-bitwarden_rs.service" ];
       description = "Backup vaultwarden on time";
       timerConfig = {
         OnCalendar = mkDefault "23:00";
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
index 496e0b90355ba..ed37c5784ce13 100644
--- a/nixos/modules/services/system/cachix-agent/default.nix
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -17,6 +17,12 @@ in {
       defaultText = "config.networking.hostName";
     };
 
+    verbose = mkOption {
+      type = types.bool;
+      description = "Enable verbose output";
+      default = false;
+    };
+
     profile = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -45,12 +51,19 @@ in {
       after = ["network-online.target"];
       path = [ config.nix.package ];
       wantedBy = [ "multi-user.target" ];
-      # don't restart while changing
-      reloadIfChanged = true;
+
+      # Cachix requires $USER to be set
+      environment.USER = "root";
+
+      # don't stop the service if the unit disappears
+      unitConfig.X-StopOnRemoval = false;
+
       serviceConfig = {
+        # we don't want to kill children processes as those are deployments
+        KillMode = "process";
         Restart = "on-failure";
         EnvironmentFile = cfg.credentialsFile;
-        ExecStart = "${cfg.package}/bin/cachix deploy agent ${cfg.name} ${if cfg.profile != null then profile else ""}";
+        ExecStart = "${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} deploy agent ${cfg.name} ${if cfg.profile != null then profile else ""}";
       };
     };
   };
diff --git a/nixos/modules/services/system/earlyoom.nix b/nixos/modules/services/system/earlyoom.nix
index 452efc736439b..6293585598907 100644
--- a/nixos/modules/services/system/earlyoom.nix
+++ b/nixos/modules/services/system/earlyoom.nix
@@ -1,124 +1,160 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
-  ecfg = config.services.earlyoom;
+  cfg = config.services.earlyoom;
+
+  inherit (lib)
+    mkDefault mkEnableOption mkIf mkOption types
+    mkRemovedOptionModule literalExpression
+    escapeShellArg concatStringsSep optional optionalString;
+
 in
 {
-  options = {
-    services.earlyoom = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable early out of memory killing.
-        '';
-      };
+  options.services.earlyoom = {
+    enable = mkEnableOption "Early out of memory killing";
 
-      freeMemThreshold = mkOption {
-        type = types.int;
-        default = 10;
-        description = ''
-          Minimum of availabe memory (in percent).
-          If the free memory falls below this threshold and the analog is true for
-          <option>services.earlyoom.freeSwapThreshold</option>
-          the killing begins.
-        '';
-      };
+    freeMemThreshold = mkOption {
+      type = types.ints.between 1 100;
+      default = 10;
+      description = ''
+        Minimum available memory (in percent).
 
-      freeSwapThreshold = mkOption {
-        type = types.int;
-        default = 10;
-        description = ''
-          Minimum of availabe swap space (in percent).
-          If the available swap space falls below this threshold and the analog
-          is true for <option>services.earlyoom.freeMemThreshold</option>
-          the killing begins.
-        '';
-      };
+        If the available memory falls below this threshold (and the analog is true for
+        <option>freeSwapThreshold</option>) the killing begins.
+        SIGTERM is sent first to the process that uses the most memory; then, if the available
+        memory falls below <option>freeMemKillThreshold</option> (and the analog is true for
+        <option>freeSwapKillThreshold</option>), SIGKILL is sent.
 
-      useKernelOOMKiller= mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Use kernel OOM killer instead of own user-space implementation.
-        '';
-      };
+        See <link xlink:href="https://github.com/rfjakob/earlyoom#command-line-options">README</link> for details.
+      '';
+    };
 
-      ignoreOOMScoreAdjust = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Ignore oom_score_adjust values of processes.
-          User-space implementation only.
-        '';
-      };
+    freeMemKillThreshold = mkOption {
+      type = types.nullOr (types.ints.between 1 100);
+      default = null;
+      description = ''
+        Minimum available memory (in percent) before sending SIGKILL.
+        If unset, this defaults to half of <option>freeMemThreshold</option>.
 
-      enableDebugInfo = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable debugging messages.
-        '';
-      };
+        See the description of <xref linkend="opt-services.earlyoom.freeMemThreshold"/>.
+      '';
+    };
 
-      notificationsCommand = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          This option is deprecated and ignored by earlyoom since 1.6.
-          Use <option>services.earlyoom.enableNotifications</option> instead.
-        '';
-      };
+    freeSwapThreshold = mkOption {
+      type = types.ints.between 1 100;
+      default = 10;
+      description = ''
+        Minimum free swap space (in percent) before sending SIGTERM.
 
-      enableNotifications = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Send notifications about killed processes via the system d-bus.
-          To actually see the notifications in your GUI session, you need to have
-          <literal>systembus-notify</literal> running as your user.
+        See the description of <xref linkend="opt-services.earlyoom.freeMemThreshold"/>.
+      '';
+    };
 
-          See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details.
-        '';
-      };
+    freeSwapKillThreshold = mkOption {
+      type = types.nullOr (types.ints.between 1 100);
+      default = null;
+      description = ''
+        Minimum free swap space (in percent) before sending SIGKILL.
+        If unset, this defaults to half of <option>freeSwapThreshold</option>.
+
+        See the description of <xref linkend="opt-services.earlyoom.freeMemThreshold"/>.
+      '';
+    };
+
+    enableDebugInfo = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable debugging messages.
+      '';
+    };
+
+    enableNotifications = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Send notifications about killed processes via the system d-bus.
+
+        WARNING: enabling this option (while convenient) should *not* be done on a
+        machine where you do not trust the other users as it allows any other
+        local user to DoS your session by spamming notifications.
+
+        To actually see the notifications in your GUI session, you need to have
+        <literal>systembus-notify</literal> running as your user, which this
+        option handles by enabling <option>services.systembus-notify</option>.
+
+        See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details.
+      '';
+    };
+
+    killHook = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = literalExpression ''
+        pkgs.writeShellScript "earlyoom-kill-hook" '''
+          echo "Process $EARLYOOM_NAME ($EARLYOOM_PID) was killed" >> /path/to/log
+        '''
+      '';
+      description = ''
+        An absolute path to an executable to be run for each process killed.
+        Some environment variables are available, see
+        <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> and
+        <link xlink:href="https://github.com/rfjakob/earlyoom/blob/master/MANPAGE.md#-n-pathtoscript">the man page</link>
+        for details.
+      '';
+    };
+
+    reportInterval = mkOption {
+      type = types.int;
+      default = 3600;
+      example = 0;
+      description = "Interval (in seconds) at which a memory report is printed (set to 0 to disable).";
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "-g" "--prefer '(^|/)(java|chromium)$'" ];
+      description = "Extra command-line arguments to be passed to earlyoom.";
     };
   };
 
-  config = mkIf ecfg.enable {
-    assertions = [
-      { assertion = ecfg.freeMemThreshold > 0 && ecfg.freeMemThreshold <= 100;
-        message = "Needs to be a positive percentage"; }
-      { assertion = ecfg.freeSwapThreshold > 0 && ecfg.freeSwapThreshold <= 100;
-        message = "Needs to be a positive percentage"; }
-      { assertion = !ecfg.useKernelOOMKiller || !ecfg.ignoreOOMScoreAdjust;
-        message = "Both options in conjunction do not make sense"; }
-    ];
+  imports = [
+    (mkRemovedOptionModule [ "services" "earlyoom" "useKernelOOMKiller" ] ''
+      This option is deprecated and ignored by earlyoom since 1.2.
+    '')
+    (mkRemovedOptionModule [ "services" "earlyoom" "notificationsCommand" ] ''
+      This option was removed in earlyoom 1.6, but was reimplemented in 1.7
+      and is available as the new option `services.earlyoom.killHook`.
+    '')
+    (mkRemovedOptionModule [ "services" "earlyoom" "ignoreOOMScoreAdjust" ] ''
+      This option is deprecated and ignored by earlyoom since 1.7.
+    '')
+  ];
 
-    warnings = optional (ecfg.notificationsCommand != null)
-      "`services.earlyoom.notificationsCommand` is deprecated and ignored by earlyoom since 1.6.";
+  config = mkIf cfg.enable {
+    services.systembus-notify.enable = mkDefault cfg.enableNotifications;
 
     systemd.services.earlyoom = {
       description = "Early OOM Daemon for Linux";
       wantedBy = [ "multi-user.target" ];
-      path = optional ecfg.enableNotifications pkgs.dbus;
+      path = optional cfg.enableNotifications pkgs.dbus;
       serviceConfig = {
-        StandardOutput = "null";
         StandardError = "journal";
-        ExecStart = ''
-          ${pkgs.earlyoom}/bin/earlyoom \
-          -m ${toString ecfg.freeMemThreshold} \
-          -s ${toString ecfg.freeSwapThreshold} \
-          ${optionalString ecfg.useKernelOOMKiller "-k"} \
-          ${optionalString ecfg.ignoreOOMScoreAdjust "-i"} \
-          ${optionalString ecfg.enableDebugInfo "-d"} \
-          ${optionalString ecfg.enableNotifications "-n"}
-        '';
+        ExecStart = concatStringsSep " " ([
+          "${pkgs.earlyoom}/bin/earlyoom"
+          ("-m ${toString cfg.freeMemThreshold}"
+            + optionalString (cfg.freeMemKillThreshold != null) ",${toString cfg.freeMemKillThreshold}")
+          ("-s ${toString cfg.freeSwapThreshold}"
+            + optionalString (cfg.freeSwapKillThreshold != null) ",${toString cfg.freeSwapKillThreshold}")
+          "-r ${toString cfg.reportInterval}"
+        ]
+        ++ optional cfg.enableDebugInfo "-d"
+        ++ optional cfg.enableNotifications "-n"
+        ++ optional (cfg.killHook != null) "-N ${escapeShellArg cfg.killHook}"
+        ++ cfg.extraArgs
+        );
       };
     };
-
-    environment.systemPackages = optional ecfg.enableNotifications pkgs.systembus-notify;
   };
 }
diff --git a/nixos/modules/services/system/localtime.nix b/nixos/modules/services/system/localtime.nix
index 8f23454af9dfd..689453375f5ac 100644
--- a/nixos/modules/services/system/localtime.nix
+++ b/nixos/modules/services/system/localtime.nix
@@ -3,30 +3,28 @@
 with lib;
 
 let
-  cfg = config.services.localtime;
+  cfg = config.services.localtimed;
 in {
+  imports = [ (lib.mkRenamedOptionModule [ "services" "localtime" ] [ "services" "localtimed" ]) ];
+
   options = {
-    services.localtime = {
+    services.localtimed = {
       enable = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Enable <literal>localtime</literal>, simple daemon for keeping the system
-          timezone up-to-date based on the current location. It uses geoclue2 to
-          determine the current location and systemd-timedated to actually set
-          the timezone.
+          Enable <literal>localtimed</literal>, a simple daemon for keeping the
+          system timezone up-to-date based on the current location. It uses
+          geoclue2 to determine the current location.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
-    services.geoclue2 = {
-      enable = true;
-      appConfig.localtime = {
-        isAllowed = true;
-        isSystem = true;
-      };
+    services.geoclue2.appConfig.localtimed = {
+      isAllowed = true;
+      isSystem = true;
     };
 
     # Install the polkit rules.
@@ -34,16 +32,6 @@ in {
     # Install the systemd unit.
     systemd.packages = [ pkgs.localtime ];
 
-    users.users.localtimed = {
-      description = "localtime daemon";
-      isSystemUser = true;
-      group = "localtimed";
-    };
-    users.groups.localtimed = {};
-
-    systemd.services.localtime = {
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig.Restart = "on-failure";
-    };
+    systemd.services.localtime.wantedBy = [ "multi-user.target" ];
   };
 }
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index 00a87e788dc4d..002c409278066 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -7,10 +7,6 @@ let
   nssModulesPath = config.system.nssModules.path;
   cfg = config.services.nscd;
 
-  nscd = if pkgs.stdenv.hostPlatform.libc == "glibc"
-         then pkgs.stdenv.cc.libc.bin
-         else pkgs.glibc.bin;
-
 in
 
 {
@@ -37,6 +33,19 @@ in
         description = "Configuration to use for Name Service Cache Daemon.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = if pkgs.stdenv.hostPlatform.libc == "glibc"
+          then pkgs.stdenv.cc.libc.bin
+          else pkgs.glibc.bin;
+        defaultText = lib.literalExpression ''
+          if pkgs.stdenv.hostPlatform.libc == "glibc"
+            then pkgs.stdenv.cc.libc.bin
+            else pkgs.glibc.bin;
+        '';
+        description = "package containing the nscd binary to be used by the service";
+      };
+
     };
 
   };
@@ -69,16 +78,16 @@ in
         # files. So prefix the ExecStart command with "!" to prevent systemd
         # from dropping privileges early. See ExecStart in systemd.service(5).
         serviceConfig =
-          { ExecStart = "!@${nscd}/sbin/nscd nscd";
+          { ExecStart = "!@${cfg.package}/bin/nscd nscd";
             Type = "forking";
             DynamicUser = true;
             RuntimeDirectory = "nscd";
             PIDFile = "/run/nscd/nscd.pid";
             Restart = "always";
             ExecReload =
-              [ "${nscd}/sbin/nscd --invalidate passwd"
-                "${nscd}/sbin/nscd --invalidate group"
-                "${nscd}/sbin/nscd --invalidate hosts"
+              [ "${cfg.package}/bin/nscd --invalidate passwd"
+                "${cfg.package}/bin/nscd --invalidate group"
+                "${cfg.package}/bin/nscd --invalidate hosts"
               ];
           };
       };
diff --git a/nixos/modules/services/system/systembus-notify.nix b/nixos/modules/services/system/systembus-notify.nix
new file mode 100644
index 0000000000000..e918bc552ece9
--- /dev/null
+++ b/nixos/modules/services/system/systembus-notify.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.systembus-notify;
+
+  inherit (lib) mkEnableOption mkIf;
+
+in
+{
+  options.services.systembus-notify = {
+    enable = mkEnableOption ''
+      System bus notification support
+
+      WARNING: enabling this option (while convenient) should *not* be done on a
+      machine where you do not trust the other users as it allows any other
+      local user to DoS your session by spamming notifications.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      packages = with pkgs; [ systembus-notify ];
+
+      user.services.systembus-notify.wantedBy = [ "graphical-session.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/tracing/tempo.nix b/nixos/modules/services/tracing/tempo.nix
new file mode 100644
index 0000000000000..15491a51c8e9e
--- /dev/null
+++ b/nixos/modules/services/tracing/tempo.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.tempo;
+
+  settingsFormat = pkgs.formats.yaml {};
+in {
+  options.services.tempo = {
+    enable = mkEnableOption "Grafana Tempo";
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
+      description = ''
+        Specify the configuration for Tempo in Nix.
+
+        See https://grafana.com/docs/tempo/latest/configuration/ for available options.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Specify a path to a configuration file that Tempo should use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # for tempo-cli and friends
+    environment.systemPackages = [ pkgs.tempo ];
+
+    assertions = [{
+      assertion = (
+        (cfg.settings == {}) != (cfg.configFile == null)
+      );
+      message  = ''
+        Please specify a configuration for Tempo with either
+        'services.tempo.settings' or
+        'services.tempo.configFile'.
+      '';
+    }];
+
+    systemd.services.tempo = {
+      description = "Grafana Tempo Service Daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = let
+        conf = if cfg.configFile == null
+               then settingsFormat.generate "config.yaml" cfg.settings
+               else cfg.configFile;
+      in
+      {
+        ExecStart = "${pkgs.tempo}/bin/tempo --config.file=${conf}";
+        DynamicUser = true;
+        Restart = "always";
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        WorkingDirectory = "/var/lib/tempo";
+        StateDirectory = "tempo";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
index 4fe720bf044bc..e02ab3cb6b323 100644
--- a/nixos/modules/services/ttys/kmscon.nix
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -1,6 +1,6 @@
 { config, pkgs, lib, ... }:
 let
-  inherit (lib) mkOption types mkIf;
+  inherit (lib) mapAttrs mkIf mkOption optional optionals types;
 
   cfg = config.services.kmscon;
 
@@ -28,6 +28,19 @@ in {
         default = false;
       };
 
+      fonts = mkOption {
+        description = "Fonts used by kmscon, in order of priority.";
+        default = null;
+        example = lib.literalExpression ''[ { name = "Source Code Pro"; package = pkgs.source-code-pro; } ]'';
+        type = with types;
+          let fontType = submodule {
+                options = {
+                  name = mkOption { type = str; description = "Font name, as used by fontconfig."; };
+                  package = mkOption { type = package; description = "Package providing the font."; };
+                };
+          }; in nullOr (nonEmptyListOf fontType);
+      };
+
       extraConfig = mkOption {
         description = "Extra contents of the kmscon.conf file.";
         type = types.lines;
@@ -87,11 +100,17 @@ in {
 
     systemd.services.systemd-vconsole-setup.enable = false;
 
-    services.kmscon.extraConfig = mkIf cfg.hwRender ''
-      drm
-      hwaccel
-    '';
+    services.kmscon.extraConfig =
+      let
+        render = optionals cfg.hwRender [ "drm" "hwaccel" ];
+        fonts = optional (cfg.fonts != null) "font-name=${lib.concatMapStringsSep ", " (f: f.name) cfg.fonts}";
+      in lib.concatStringsSep "\n" (render ++ fonts);
 
     hardware.opengl.enable = mkIf cfg.hwRender true;
+
+    fonts = mkIf (cfg.fonts != null) {
+      fontconfig.enable = true;
+      fonts = map (f: f.package) cfg.fonts;
+    };
   };
 }
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix
index 41613dcbb3ba4..191f6eb52e57e 100644
--- a/nixos/modules/services/video/epgstation/default.nix
+++ b/nixos/modules/services/video/epgstation/default.nix
@@ -1,30 +1,40 @@
 { config, lib, options, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.epgstation;
   opt = options.services.epgstation;
 
+  description = "EPGStation: DVR system for Mirakurun-managed TV tuners";
+
   username = config.users.users.epgstation.name;
   groupname = config.users.users.epgstation.group;
+  mirakurun = {
+    sock = config.services.mirakurun.unixSocket;
+    option = options.services.mirakurun.unixSocket;
+  };
 
-  settingsFmt = pkgs.formats.json {};
-  settingsTemplate = settingsFmt.generate "config.json" cfg.settings;
+  yaml = pkgs.formats.yaml { };
+  settingsTemplate = yaml.generate "config.yml" cfg.settings;
   preStartScript = pkgs.writeScript "epgstation-prestart" ''
     #!${pkgs.runtimeShell}
 
-    PASSWORD="$(head -n1 "${cfg.basicAuth.passwordFile}")"
-    DB_PASSWORD="$(head -n1 "${cfg.database.passwordFile}")"
+    DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile}
+
+    if [[ ! -f "$DB_PASSWORD_FILE" ]]; then
+      printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \
+        "$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2
+      exit 1
+    fi
+
+    DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})"
 
     # setup configuration
-    touch /etc/epgstation/config.json
-    chmod 640 /etc/epgstation/config.json
+    touch /etc/epgstation/config.yml
+    chmod 640 /etc/epgstation/config.yml
     sed \
-      -e "s,@password@,$PASSWORD,g" \
       -e "s,@dbPassword@,$DB_PASSWORD,g" \
-      ${settingsTemplate} > /etc/epgstation/config.json
-    chown "${username}:${groupname}" /etc/epgstation/config.json
+      ${settingsTemplate} > /etc/epgstation/config.yml
+    chown "${username}:${groupname}" /etc/epgstation/config.yml
 
     # NOTE: Use password authentication, since mysqljs does not yet support auth_socket
     if [ ! -e /var/lib/epgstation/db-created ]; then
@@ -35,7 +45,7 @@ let
   '';
 
   streamingConfig = lib.importJSON ./streaming.json;
-  logConfig = {
+  logConfig = yaml.generate "logConfig.yml" {
     appenders.stdout.type = "stdout";
     categories = {
       default = { appenders = [ "stdout" ]; level = "info"; };
@@ -45,53 +55,51 @@ let
     };
   };
 
-  defaultPassword = "INSECURE_GO_CHECK_CONFIGURATION_NIX\n";
+  # Deprecate top level options that are redundant.
+  deprecateTopLevelOption = config:
+    lib.mkRenamedOptionModule
+      ([ "services" "epgstation" ] ++ config)
+      ([ "services" "epgstation" "settings" ] ++ config);
+
+  removeOption = config: instruction:
+    lib.mkRemovedOptionModule
+      ([ "services" "epgstation" ] ++ config)
+      instruction;
 in
 {
-  options.services.epgstation = {
-    enable = mkEnableOption "EPGStation: DTV Software in Japan";
+  meta.maintainers = with lib.maintainers; [ midchildan ];
 
-    usePreconfiguredStreaming = mkOption {
-      type = types.bool;
-      default = true;
-      description = ''
-        Use preconfigured default streaming options.
+  imports = [
+    (deprecateTopLevelOption [ "port" ])
+    (deprecateTopLevelOption [ "socketioPort" ])
+    (deprecateTopLevelOption [ "clientSocketioPort" ])
+    (removeOption [ "basicAuth" ]
+      "Use a TLS-terminated reverse proxy with authentication instead.")
+  ];
 
-        Upstream defaults:
-        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.sample.json"/>
-      '';
-    };
+  options.services.epgstation = {
+    enable = lib.mkEnableOption description;
 
-    port = mkOption {
-      type = types.port;
-      default = 20772;
-      description = ''
-        HTTP port for EPGStation to listen on.
-      '';
+    package = lib.mkOption {
+      default = pkgs.epgstation;
+      type = lib.types.package;
+      defaultText = lib.literalExpression "pkgs.epgstation";
+      description = "epgstation package to use";
     };
 
-    socketioPort = mkOption {
-      type = types.port;
-      default = cfg.port + 1;
-      defaultText = literalExpression "config.${opt.port} + 1";
+    usePreconfiguredStreaming = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
       description = ''
-        Socket.io port for EPGStation to listen on.
-      '';
-    };
+        Use preconfigured default streaming options.
 
-    clientSocketioPort = mkOption {
-      type = types.port;
-      default = cfg.socketioPort;
-      defaultText = literalExpression "config.${opt.socketioPort}";
-      description = ''
-        Socket.io port that the web client is going to connect to. This may be
-        different from <option>socketioPort</option> if EPGStation is hidden
-        behind a reverse proxy.
+        Upstream defaults:
+        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template"/>
       '';
     };
 
-    openFirewall = mkOption {
-      type = types.bool;
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
       default = false;
       description = ''
         Open ports in the firewall for the EPGStation web interface.
@@ -106,50 +114,17 @@ in
       '';
     };
 
-    basicAuth = {
-      user = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        example = "epgstation";
-        description = ''
-          Basic auth username for EPGStation. If <literal>null</literal>, basic
-          auth will be disabled.
-
-          <warning>
-            <para>
-              Basic authentication has known weaknesses, the most critical being
-              that it sends passwords over the network in clear text. Use this
-              feature to control access to EPGStation within your family and
-              friends, but don't rely on it for security.
-            </para>
-          </warning>
-        '';
-      };
-
-      passwordFile = mkOption {
-        type = types.path;
-        default = pkgs.writeText "epgstation-password" defaultPassword;
-        defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>'';
-        example = "/run/keys/epgstation-password";
-        description = ''
-          A file containing the password for <option>basicAuth.user</option>.
-        '';
-      };
-    };
-
-    database =  {
-      name = mkOption {
-        type = types.str;
+    database = {
+      name = lib.mkOption {
+        type = lib.types.str;
         default = "epgstation";
         description = ''
           Name of the MySQL database that holds EPGStation's data.
         '';
       };
 
-      passwordFile = mkOption {
-        type = types.path;
-        default = pkgs.writeText "epgstation-db-password" defaultPassword;
-        defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>'';
+      passwordFile = lib.mkOption {
+        type = lib.types.path;
         example = "/run/keys/epgstation-db-password";
         description = ''
           A file containing the password for the database named
@@ -158,69 +133,106 @@ in
       };
     };
 
-    settings = mkOption {
+    # The defaults for some options come from the upstream template
+    # configuration, which is the one that users would get if they follow the
+    # upstream instructions. This is, in some cases, different from the
+    # application defaults. Some options like encodeProcessNum and
+    # concurrentEncodeNum doesn't have an optimal default value that works for
+    # all hardware setups and/or performance requirements. For those kind of
+    # options, the application default wouldn't always result in the expected
+    # out-of-the-box behavior because it's the responsibility of the user to
+    # configure them according to their needs. In these cases, the value in the
+    # upstream template configuration should serve as a "good enough" default.
+    settings = lib.mkOption {
       description = ''
-        Options to add to config.json.
+        Options to add to config.yml.
 
         Documentation:
         <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/>
       '';
 
-      default = {};
+      default = { };
       example = {
         recPriority = 20;
         conflictPriority = 10;
       };
 
-      type = types.submodule {
-        freeformType = settingsFmt.type;
+      type = lib.types.submodule {
+        freeformType = yaml.type;
+
+        options.port = lib.mkOption {
+          type = lib.types.port;
+          default = 20772;
+          description = ''
+            HTTP port for EPGStation to listen on.
+          '';
+        };
 
-        options.readOnlyOnce = mkOption {
-          type = types.bool;
-          default = false;
-          description = "Don't reload configuration files at runtime.";
+        options.socketioPort = lib.mkOption {
+          type = lib.types.port;
+          default = cfg.settings.port + 1;
+          defaultText = lib.literalExpression "config.${opt.settings}.port + 1";
+          description = ''
+            Socket.io port for EPGStation to listen on. It is valid to share
+            ports with <option>${opt.settings}.port</option>.
+          '';
         };
 
-        options.mirakurunPath = mkOption (let
-          sockPath = config.services.mirakurun.unixSocket;
-        in {
-          type = types.str;
-          default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}";
-          defaultText = literalExpression ''
-            "http+unix://''${replaceStrings ["/"] ["%2F"] config.${options.services.mirakurun.unixSocket}}"
+        options.clientSocketioPort = lib.mkOption {
+          type = lib.types.port;
+          default = cfg.settings.socketioPort;
+          defaultText = lib.literalExpression "config.${opt.settings}.socketioPort";
+          description = ''
+            Socket.io port that the web client is going to connect to. This may
+            be different from <option>${opt.settings}.socketioPort</option> if
+            EPGStation is hidden behind a reverse proxy.
+          '';
+        };
+
+        options.mirakurunPath = with mirakurun; lib.mkOption {
+          type = lib.types.str;
+          default = "http+unix://${lib.replaceStrings ["/"] ["%2F"] sock}";
+          defaultText = lib.literalExpression ''
+            "http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}"
           '';
           example = "http://localhost:40772";
           description = "URL to connect to Mirakurun.";
-        });
+        };
+
+        options.encodeProcessNum = lib.mkOption {
+          type = lib.types.ints.positive;
+          default = 4;
+          description = ''
+            The maximum number of processes that EPGStation would allow to run
+            at the same time for encoding or streaming videos.
+          '';
+        };
+
+        options.concurrentEncodeNum = lib.mkOption {
+          type = lib.types.ints.positive;
+          default = 1;
+          description = ''
+            The maximum number of encoding jobs that EPGStation would run at the
+            same time.
+          '';
+        };
 
-        options.encode = mkOption {
-          type = with types; listOf attrs;
+        options.encode = lib.mkOption {
+          type = with lib.types; listOf attrs;
           description = "Encoding presets for recorded videos.";
           default = [
             {
-              name = "H264";
-              cmd = "${pkgs.epgstation}/libexec/enc.sh main";
+              name = "H.264";
+              cmd = "%NODE% ${cfg.package}/libexec/enc.js";
               suffix = ".mp4";
-              default = true;
-            }
-            {
-              name = "H264-sub";
-              cmd = "${pkgs.epgstation}/libexec/enc.sh sub";
-              suffix = "-sub.mp4";
             }
           ];
-          defaultText = literalExpression ''
+          defaultText = lib.literalExpression ''
             [
               {
-                name = "H264";
-                cmd = "''${pkgs.epgstation}/libexec/enc.sh main";
+                name = "H.264";
+                cmd = "%NODE% config.${opt.package}/libexec/enc.js";
                 suffix = ".mp4";
-                default = true;
-              }
-              {
-                name = "H264-sub";
-                cmd = "''${pkgs.epgstation}/libexec/enc.sh sub";
-                suffix = "-sub.mp4";
               }
             ]
           '';
@@ -229,14 +241,25 @@ in
     };
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings);
+        message = ''
+          The option config.${opt.settings}.readOnlyOnce can no longer be used
+          since it's been removed. No replacements are available.
+        '';
+      }
+    ];
+
     environment.etc = {
-      "epgstation/operatorLogConfig.json".text = builtins.toJSON logConfig;
-      "epgstation/serviceLogConfig.json".text = builtins.toJSON logConfig;
+      "epgstation/epgUpdaterLogConfig.yml".source = logConfig;
+      "epgstation/operatorLogConfig.yml".source = logConfig;
+      "epgstation/serviceLogConfig.yml".source = logConfig;
     };
 
-    networking.firewall = mkIf cfg.openFirewall {
-      allowedTCPPorts = with cfg; [ port socketioPort ];
+    networking.firewall = lib.mkIf cfg.openFirewall {
+      allowedTCPPorts = with cfg.settings; [ port socketioPort ];
     };
 
     users.users.epgstation = {
@@ -245,13 +268,13 @@ in
       isSystemUser = true;
     };
 
-    users.groups.epgstation = {};
+    users.groups.epgstation = { };
 
-    services.mirakurun.enable = mkDefault true;
+    services.mirakurun.enable = lib.mkDefault true;
 
     services.mysql = {
-      enable = mkDefault true;
-      package = mkDefault pkgs.mariadb;
+      enable = lib.mkDefault true;
+      package = lib.mkDefault pkgs.mariadb;
       ensureDatabases = [ cfg.database.name ];
       # FIXME: enable once mysqljs supports auth_socket
       # ensureUsers = [ {
@@ -260,39 +283,28 @@ in
       # } ];
     };
 
-    services.epgstation.settings = let
-      defaultSettings = {
-        serverPort = cfg.port;
-        socketioPort = cfg.socketioPort;
-        clientSocketioPort = cfg.clientSocketioPort;
-
-        dbType = mkDefault "mysql";
-        mysql = {
-          user = username;
-          database = cfg.database.name;
-          socketPath = mkDefault "/run/mysqld/mysqld.sock";
-          password = mkDefault "@dbPassword@";
-          connectTimeout = mkDefault 1000;
-          connectionLimit = mkDefault 10;
+    services.epgstation.settings =
+      let
+        defaultSettings = {
+          dbtype = lib.mkDefault "mysql";
+          mysql = {
+            socketPath = lib.mkDefault "/run/mysqld/mysqld.sock";
+            user = username;
+            password = lib.mkDefault "@dbPassword@";
+            database = cfg.database.name;
+          };
+
+          ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
+          ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
+
+          # for disambiguation with TypeScript files
+          recordedFileExtension = lib.mkDefault ".m2ts";
         };
-
-        basicAuth = mkIf (cfg.basicAuth.user != null) {
-          user = mkDefault cfg.basicAuth.user;
-          password = mkDefault "@password@";
-        };
-
-        ffmpeg = mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
-        ffprobe = mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
-
-        fileExtension = mkDefault ".m2ts";
-        maxEncode = mkDefault 2;
-        maxStreaming = mkDefault 2;
-      };
-    in
-    mkMerge [
-      defaultSettings
-      (mkIf cfg.usePreconfiguredStreaming streamingConfig)
-    ];
+      in
+      lib.mkMerge [
+        defaultSettings
+        (lib.mkIf cfg.usePreconfiguredStreaming streamingConfig)
+      ];
 
     systemd.tmpfiles.rules = [
       "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
@@ -301,15 +313,15 @@ in
     ];
 
     systemd.services.epgstation = {
-      description = pkgs.epgstation.meta.description;
+      inherit description;
+
       wantedBy = [ "multi-user.target" ];
-      after = [
-        "network.target"
-      ] ++ optional config.services.mirakurun.enable "mirakurun.service"
-        ++ optional config.services.mysql.enable "mysql.service";
+      after = [ "network.target" ]
+        ++ lib.optional config.services.mirakurun.enable "mirakurun.service"
+        ++ lib.optional config.services.mysql.enable "mysql.service";
 
       serviceConfig = {
-        ExecStart = "${pkgs.epgstation}/bin/epgstation start";
+        ExecStart = "${cfg.package}/bin/epgstation start";
         ExecStartPre = "+${preStartScript}";
         User = username;
         Group = groupname;
diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json
index 8eb99cf85584b..7f8df0817fc3d 100644
--- a/nixos/modules/services/video/epgstation/streaming.json
+++ b/nixos/modules/services/video/epgstation/streaming.json
@@ -1,119 +1,140 @@
 {
-  "liveHLS": [
-    {
-      "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%"
+  "urlscheme": {
+    "m2ts": {
+      "ios": "vlc-x-callback://x-callback-url/stream?url=PROTOCOL://ADDRESS",
+      "android": "intent://ADDRESS#Intent;package=org.videolan.vlc;type=video;scheme=PROTOCOL;end"
     },
-    {
-      "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%"
+    "video": {
+      "ios": "infuse://x-callback-url/play?url=PROTOCOL://ADDRESS",
+      "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=PROTOCOL;end"
     },
-    {
-      "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%"
+    "download": {
+      "ios": "vlc-x-callback://x-callback-url/download?url=PROTOCOL://ADDRESS&filename=FILENAME"
     }
-  ],
-  "liveMP4": [
-    {
-      "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"
-    },
-    {
-      "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": [
-    {
-      "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"
-    },
-    {
-      "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": [
-    {
-      "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"
-    },
-    {
-      "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": {
-    "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": {
-    "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"
   },
-  "recordedStreaming": {
-    "webm": [
-      {
-        "name": "720p",
-        "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"
-      },
-      {
-        "name": "360p",
-        "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"
-      }
-    ],
-    "mp4": [
-      {
-        "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"
-      },
-      {
-        "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"
+  "stream": {
+    "live": {
+      "ts": {
+        "m2ts": [
+          {
+            "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"
+          },
+          {
+            "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": "無変換"
+          }
+        ],
+        "m2tsll": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
+          }
+        ],
+        "webm": [
+          {
+            "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"
+          },
+          {
+            "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"
+          }
+        ],
+        "mp4": [
+          {
+            "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"
+          },
+          {
+            "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"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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%"
+          }
+        ]
       }
-    ],
-    "mpegTs": [
-      {
-        "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"
+    },
+    "recorded": {
+      "ts": {
+        "webm": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -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": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -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"
+          }
+        ],
+        "mp4": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -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": "480p",
+            "cmd": "%FFMPEG% -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"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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 pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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": "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"
+      "encoded": {
+        "webm": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          }
+        ],
+        "mp4": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf 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": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf 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"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 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 -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 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 -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          }
+        ]
       }
-    ]
-  },
-  "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": {
-    "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/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
index 43208a9fe4cfb..11d9fe305470c 100644
--- a/nixos/modules/services/video/unifi-video.nix
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -16,7 +16,7 @@ let
     -pidfile ${cfg.pidFile} \
     -procname unifi-video \
     -Djava.security.egd=file:/dev/./urandom \
-    -Xmx${cfg.maximumJavaHeapSize}M \
+    -Xmx${toString cfg.maximumJavaHeapSize}M \
     -Xss512K \
     -XX:+UseG1GC \
     -XX:+UseStringDeduplication \
@@ -91,98 +91,102 @@ let
   stateDir = "/var/lib/unifi-video";
 
 in
-  {
-
-    options.services.unifi-video = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether or not to enable the unifi-video service.
-        '';
-      };
+{
 
-      jrePackage = mkOption {
-        type = types.package;
-        default = pkgs.jre8;
-        defaultText = literalExpression "pkgs.jre8";
-        description = ''
-          The JRE package to use. Check the release notes to ensure it is supported.
-        '';
-      };
+  options.services.unifi-video = {
 
-      unifiVideoPackage = mkOption {
-        type = types.package;
-        default = pkgs.unifi-video;
-        defaultText = literalExpression "pkgs.unifi-video";
-        description = ''
-          The unifi-video package to use.
-        '';
-      };
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether or not to enable the unifi-video service.
+      '';
+    };
 
-      mongodbPackage = mkOption {
-        type = types.package;
-        default = pkgs.mongodb-4_0;
-        defaultText = literalExpression "pkgs.mongodb";
-        description = ''
-          The mongodb package to use.
-        '';
-      };
+    jrePackage = mkOption {
+      type = types.package;
+      default = pkgs.jre8;
+      defaultText = literalExpression "pkgs.jre8";
+      description = ''
+        The JRE package to use. Check the release notes to ensure it is supported.
+      '';
+    };
 
-      logDir = mkOption {
-        type = types.str;
-        default = "${stateDir}/logs";
-        description = ''
-          Where to store the logs.
-        '';
-      };
+    unifiVideoPackage = mkOption {
+      type = types.package;
+      default = pkgs.unifi-video;
+      defaultText = literalExpression "pkgs.unifi-video";
+      description = ''
+        The unifi-video package to use.
+      '';
+    };
 
-      dataDir = mkOption {
-        type = types.str;
-        default = "${stateDir}/data";
-        description = ''
-          Where to store the database and other data.
-        '';
-      };
+    mongodbPackage = mkOption {
+      type = types.package;
+      default = pkgs.mongodb-4_0;
+      defaultText = literalExpression "pkgs.mongodb";
+      description = ''
+        The mongodb package to use.
+      '';
+    };
 
-      openPorts = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether or not to open the required ports on the firewall.
-        '';
-      };
+    logDir = mkOption {
+      type = types.str;
+      default = "${stateDir}/logs";
+      description = ''
+        Where to store the logs.
+      '';
+    };
 
-      maximumJavaHeapSize = mkOption {
-        type = types.nullOr types.int;
-        default = 1024;
-        example = 4096;
-        description = ''
-          Set the maximimum heap size for the JVM in MB.
-        '';
-      };
+    dataDir = mkOption {
+      type = types.str;
+      default = "${stateDir}/data";
+      description = ''
+        Where to store the database and other data.
+      '';
+    };
 
-      pidFile = mkOption {
-        type = types.path;
-        default = "${cfg.dataDir}/unifi-video.pid";
-        defaultText = literalExpression ''"''${config.${opt.dataDir}}/unifi-video.pid"'';
-        description = "Location of unifi-video pid file.";
-      };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether or not to open the required ports on the firewall.
+      '';
+    };
+
+    maximumJavaHeapSize = mkOption {
+      type = types.nullOr types.int;
+      default = 1024;
+      example = 4096;
+      description = ''
+        Set the maximimum heap size for the JVM in MB.
+      '';
+    };
+
+    pidFile = mkOption {
+      type = types.path;
+      default = "${cfg.dataDir}/unifi-video.pid";
+      defaultText = literalExpression ''"''${config.${opt.dataDir}}/unifi-video.pid"'';
+      description = "Location of unifi-video pid file.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
 
-};
+    warnings = optional
+      (options.services.unifi-video.openFirewall.highestPrio >= (mkOptionDefault null).priority)
+      "The current services.unifi-video.openFirewall = true default is deprecated and will change to false in 22.11. Set it explicitly to silence this warning.";
 
-config = mkIf cfg.enable {
-  users = {
-    users.unifi-video = {
+    users.users.unifi-video = {
       description = "UniFi Video controller daemon user";
       home = stateDir;
       group = "unifi-video";
       isSystemUser = true;
     };
-    groups.unifi-video = {};
-  };
+    users.groups.unifi-video = {};
 
-  networking.firewall = mkIf cfg.openPorts {
+    networking.firewall = mkIf cfg.openFirewall {
       # https://help.ui.com/hc/en-us/articles/217875218-UniFi-Video-Ports-Used
       allowedTCPPorts = [
         7080 # HTTP portal
@@ -237,7 +241,6 @@ config = mkIf cfg.enable {
       "L+ '${stateDir}/conf/server.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/server.xml"
       "L+ '${stateDir}/conf/tomcat-users.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/tomcat-users.xml"
       "L+ '${stateDir}/conf/web.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/web.xml"
-
     ];
 
     systemd.services.unifi-video = {
@@ -258,10 +261,11 @@ config = mkIf cfg.enable {
         WorkingDirectory = "${stateDir}";
       };
     };
-
   };
 
-  meta = {
-    maintainers = with lib.maintainers; [ rsynnest ];
-  };
+  imports = [
+    (mkRenamedOptionModule [ "services" "unifi-video" "openPorts" ] [ "services" "unifi-video" "openFirewall" ])
+  ];
+
+  meta.maintainers = with lib.maintainers; [ rsynnest ];
 }
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
index d2bbc4fc057b3..b818f5c463a91 100644
--- a/nixos/modules/services/wayland/cage.nix
+++ b/nixos/modules/services/wayland/cage.nix
@@ -81,12 +81,14 @@ in {
       };
     };
 
+    security.polkit.enable = true;
+
     security.pam.services.cage.text = ''
       auth    required pam_unix.so nullok
       account required pam_unix.so
       session required pam_unix.so
       session required pam_env.so conffile=/etc/pam/environment readenv=0
-      session required ${pkgs.systemd}/lib/security/pam_systemd.so
+      session required ${config.systemd.package}/lib/security/pam_systemd.so
     '';
 
     hardware.opengl.enable = mkDefault true;
diff --git a/nixos/modules/services/web-apps/atlassian/confluence.nix b/nixos/modules/services/web-apps/atlassian/confluence.nix
index 2d809c17ff098..4aad307731ab3 100644
--- a/nixos/modules/services/web-apps/atlassian/confluence.nix
+++ b/nixos/modules/services/web-apps/atlassian/confluence.nix
@@ -8,21 +8,22 @@ let
 
   pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
-    crowdProperties = ''
-      application.name                        ${cfg.sso.applicationName}
-      application.password                    ${cfg.sso.applicationPassword}
-      application.login.url                   ${cfg.sso.crowd}/console/
-
-      crowd.server.url                        ${cfg.sso.crowd}/services/
-      crowd.base.url                          ${cfg.sso.crowd}/
-
-      session.isauthenticated                 session.isauthenticated
-      session.tokenkey                        session.tokenkey
-      session.validationinterval              ${toString cfg.sso.validationInterval}
-      session.lastvalidation                  session.lastvalidation
-    '';
   });
 
+  crowdProperties = pkgs.writeText "crowd.properties" ''
+    application.name                        ${cfg.sso.applicationName}
+    application.password                    ${if cfg.sso.applicationPassword != null then cfg.sso.applicationPassword else "@NIXOS_CONFLUENCE_CROWD_SSO_PWD@"}
+    application.login.url                   ${cfg.sso.crowd}/console/
+
+    crowd.server.url                        ${cfg.sso.crowd}/services/
+    crowd.base.url                          ${cfg.sso.crowd}/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              ${toString cfg.sso.validationInterval}
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -107,10 +108,17 @@ in
         };
 
         applicationPassword = mkOption {
-          type = types.str;
+          type = types.nullOr types.str;
+          default = null;
           description = "Application password of this Confluence instance in Crowd";
         };
 
+        applicationPasswordFile = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = "Path to the application password for Crowd of Confluence.";
+        };
+
         validationInterval = mkOption {
           type = types.int;
           default = 2;
@@ -147,6 +155,16 @@ in
       group = cfg.group;
     };
 
+    assertions = [
+      { assertion = cfg.sso.enable -> ((cfg.sso.applicationPassword == null) != (cfg.sso.applicationPasswordFile));
+        message = "Please set either applicationPassword or applicationPasswordFile";
+      }
+    ];
+
+    warnings = mkIf (cfg.sso.enable && cfg.sso.applicationPassword != null) [
+      "Using `services.confluence.sso.applicationPassword` is deprecated! Use `applicationPasswordFile` instead!"
+    ];
+
     users.groups.${cfg.group} = {};
 
     systemd.tmpfiles.rules = [
@@ -173,6 +191,7 @@ in
         CONF_USER = cfg.user;
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+        JAVA_OPTS = mkIf cfg.sso.enable "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -183,12 +202,24 @@ in
           -e 's,protocol="org.apache.coyote.http11.Http11NioProtocol",protocol="org.apache.coyote.http11.Http11NioProtocol" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}",' \
         '') + ''
           ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString cfg.sso.enable ''
+          install -m660 ${crowdProperties} ${cfg.home}/crowd.properties
+          ${optionalString (cfg.sso.applicationPasswordFile != null) ''
+            ${pkgs.replace-secret}/bin/replace-secret \
+              '@NIXOS_CONFLUENCE_CROWD_SSO_PWD@' \
+              ${cfg.sso.applicationPasswordFile} \
+              ${cfg.home}/crowd.properties
+          ''}
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/bin/start-confluence.sh -fg";
         ExecStop = "${pkg}/bin/stop-confluence.sh";
       };
diff --git a/nixos/modules/services/web-apps/atlassian/crowd.nix b/nixos/modules/services/web-apps/atlassian/crowd.nix
index a8b2482d5a9cc..9418aff12adb3 100644
--- a/nixos/modules/services/web-apps/atlassian/crowd.nix
+++ b/nixos/modules/services/web-apps/atlassian/crowd.nix
@@ -14,6 +14,21 @@ let
     proxyUrl = "${cfg.proxy.scheme}://${cfg.proxy.name}:${toString cfg.proxy.port}";
   });
 
+  crowdPropertiesFile = pkgs.writeText "crowd.properties" ''
+    application.name                        crowd-openid-server
+    application.password @NIXOS_CROWD_OPENID_PW@
+    application.base.url                    http://localhost:${toString cfg.listenPort}/openidserver
+    application.login.url                   http://localhost:${toString cfg.listenPort}/openidserver
+    application.login.url.template          http://localhost:${toString cfg.listenPort}/openidserver?returnToUrl=''${RETURN_TO_URL}
+
+    crowd.server.url                        http://localhost:${toString cfg.listenPort}/crowd/services/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              0
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -53,9 +68,16 @@ in
 
       openidPassword = mkOption {
         type = types.str;
+        default = "WILL_NEVER_BE_SET";
         description = "Application password for OpenID server.";
       };
 
+      openidPasswordFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Path to the file containing the application password for OpenID server.";
+      };
+
       catalinaOptions = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -140,6 +162,7 @@ in
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
         CATALINA_TMPDIR = "/tmp";
+        JAVA_OPTS = mkIf (cfg.openidPasswordFile != null) "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -151,12 +174,22 @@ in
           -e 's,compression="on",compression="off" protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${boolToString cfg.proxy.secure}",' \
         '') + ''
           ${pkg}/apache-tomcat/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString (cfg.openidPasswordFile != null) ''
+          install -m660 ${crowdPropertiesFile} ${cfg.home}/crowd.properties
+          ${pkgs.replace-secret}/bin/replace-secret \
+            '@NIXOS_CROWD_OPENID_PW@' \
+            ${cfg.openidPasswordFile} \
+            ${cfg.home}/crowd.properties
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/start_crowd.sh -fg";
       };
     };
diff --git a/nixos/modules/services/web-apps/atlassian/jira.nix b/nixos/modules/services/web-apps/atlassian/jira.nix
index d7a26838d6f82..fd89d763c7fb4 100644
--- a/nixos/modules/services/web-apps/atlassian/jira.nix
+++ b/nixos/modules/services/web-apps/atlassian/jira.nix
@@ -8,21 +8,22 @@ let
 
   pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
-    crowdProperties = ''
-      application.name                        ${cfg.sso.applicationName}
-      application.password                    ${cfg.sso.applicationPassword}
-      application.login.url                   ${cfg.sso.crowd}/console/
-
-      crowd.server.url                        ${cfg.sso.crowd}/services/
-      crowd.base.url                          ${cfg.sso.crowd}/
-
-      session.isauthenticated                 session.isauthenticated
-      session.tokenkey                        session.tokenkey
-      session.validationinterval              ${toString cfg.sso.validationInterval}
-      session.lastvalidation                  session.lastvalidation
-    '';
   });
 
+  crowdProperties = pkgs.writeText "crowd.properties" ''
+    application.name                        ${cfg.sso.applicationName}
+    application.password                    @NIXOS_JIRA_CROWD_SSO_PWD@
+    application.login.url                   ${cfg.sso.crowd}/console/
+
+    crowd.server.url                        ${cfg.sso.crowd}/services/
+    crowd.base.url                          ${cfg.sso.crowd}/
+
+    session.isauthenticated                 session.isauthenticated
+    session.tokenkey                        session.tokenkey
+    session.validationinterval              ${toString cfg.sso.validationInterval}
+    session.lastvalidation                  session.lastvalidation
+  '';
+
 in
 
 {
@@ -112,9 +113,9 @@ in
           description = "Exact name of this JIRA instance in Crowd";
         };
 
-        applicationPassword = mkOption {
+        applicationPasswordFile = mkOption {
           type = types.str;
-          description = "Application password of this JIRA instance in Crowd";
+          description = "Path to the file containing the application password of this JIRA instance in Crowd";
         };
 
         validationInterval = mkOption {
@@ -151,6 +152,7 @@ in
     users.users.${cfg.user} = {
       isSystemUser = true;
       group = cfg.group;
+      home = cfg.home;
     };
 
     users.groups.${cfg.group} = {};
@@ -180,6 +182,7 @@ in
         JIRA_HOME = cfg.home;
         JAVA_HOME = "${cfg.jrePackage}";
         CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+        JAVA_OPTS = mkIf cfg.sso.enable "-Dcrowd.properties=${cfg.home}/crowd.properties";
       };
 
       preStart = ''
@@ -190,15 +193,31 @@ in
           -e 's,protocol="HTTP/1.1",protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${toString cfg.proxy.secure}",' \
         '') + ''
           ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+
+        ${optionalString cfg.sso.enable ''
+          install -m660 ${crowdProperties} ${cfg.home}/crowd.properties
+          ${pkgs.replace-secret}/bin/replace-secret \
+            '@NIXOS_JIRA_CROWD_SSO_PWD@' \
+            ${cfg.sso.applicationPasswordFile} \
+            ${cfg.home}/crowd.properties
+        ''}
       '';
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         PrivateTmp = true;
+        Restart = "on-failure";
+        RestartSec = "10";
         ExecStart = "${pkg}/bin/start-jira.sh -fg";
         ExecStop = "${pkg}/bin/stop-jira.sh";
       };
     };
   };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "jira" "sso" "applicationPassword" ] ''
+      Use `applicationPasswordFile` instead!
+    '')
+  ];
 }
diff --git a/nixos/modules/services/web-apps/calibre-web.nix b/nixos/modules/services/web-apps/calibre-web.nix
index 704cd2cfa8a7c..2bc817343a9b7 100644
--- a/nixos/modules/services/web-apps/calibre-web.nix
+++ b/nixos/modules/services/web-apps/calibre-web.nix
@@ -136,7 +136,7 @@ in
 
               ${pkgs.sqlite}/bin/sqlite3 ${appDb} "update settings set ${settings}"
             '' + optionalString (cfg.options.calibreLibrary != null) ''
-              test -f ${cfg.options.calibreLibrary}/metadata.db || { echo "Invalid Calibre library"; exit 1; }
+              test -f "${cfg.options.calibreLibrary}/metadata.db" || { echo "Invalid Calibre library"; exit 1; }
             ''
           );
 
diff --git a/nixos/modules/services/web-apps/cryptpad.nix b/nixos/modules/services/web-apps/cryptpad.nix
deleted file mode 100644
index e6772de768e0e..0000000000000
--- a/nixos/modules/services/web-apps/cryptpad.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.cryptpad;
-in
-{
-  options.services.cryptpad = {
-    enable = mkEnableOption "the Cryptpad service";
-
-    package = mkOption {
-      default = pkgs.cryptpad;
-      defaultText = literalExpression "pkgs.cryptpad";
-      type = types.package;
-      description = "
-        Cryptpad package to use.
-      ";
-    };
-
-    configFile = mkOption {
-      type = types.path;
-      default = "${cfg.package}/lib/node_modules/cryptpad/config/config.example.js";
-      defaultText = literalExpression ''"''${package}/lib/node_modules/cryptpad/config/config.example.js"'';
-      description = ''
-        Path to the JavaScript configuration file.
-
-        See <link
-        xlink:href="https://github.com/xwiki-labs/cryptpad/blob/master/config/config.example.js"/>
-        for a configuration example.
-      '';
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.cryptpad = {
-      description = "Cryptpad Service";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "networking.target" ];
-      serviceConfig = {
-        DynamicUser = true;
-        Environment = [
-          "CRYPTPAD_CONFIG=${cfg.configFile}"
-          "HOME=%S/cryptpad"
-        ];
-        ExecStart = "${cfg.package}/bin/cryptpad";
-        PrivateTmp = true;
-        Restart = "always";
-        StateDirectory = "cryptpad";
-        WorkingDirectory = "%S/cryptpad";
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index 2c2911aada3f1..7dbbf4a12fe5b 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -609,6 +609,7 @@ in
       connection_reaper_interval = 30;
       relative_url_root = null;
       message_bus_max_backlog_size = 100;
+      message_bus_clear_every = 50;
       secret_key_base = cfg.secretKeyBaseFile;
       fallback_assets_path = null;
 
@@ -655,7 +656,12 @@ in
       long_polling_interval = null;
     };
 
-    services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
+    services.redis.servers.discourse =
+      lib.mkIf (lib.elem cfg.redis.host [ "localhost" "127.0.0.1" ]) {
+        enable = true;
+        bind = cfg.redis.host;
+        port = cfg.backendSettings.redis_port;
+      };
 
     services.postgresql = lib.mkIf databaseActuallyCreateLocally {
       enable = true;
@@ -696,12 +702,12 @@ in
     systemd.services.discourse = {
       wantedBy = [ "multi-user.target" ];
       after = [
-        "redis.service"
+        "redis-discourse.service"
         "postgresql.service"
         "discourse-postgresql.service"
       ];
       bindsTo = [
-        "redis.service"
+        "redis-discourse.service"
       ] ++ lib.optionals (cfg.database.host == null) [
         "postgresql.service"
         "discourse-postgresql.service"
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index 30913ced88493..d8fc978774e48 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -293,9 +293,7 @@ in
         inherit user;
         group = webserver.group;
 
-        # Not yet compatible with php 8 https://www.dokuwiki.org/requirements
-        # https://github.com/splitbrain/dokuwiki/issues/3545
-        phpPackage = pkgs.php74;
+        phpPackage = pkgs.php81;
         phpEnv = {
           DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
           DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
@@ -376,7 +374,7 @@ in
           "~ \\.php$" = {
             extraConfig = ''
               try_files $uri $uri/ /doku.php;
-              include ${pkgs.nginx}/conf/fastcgi_params;
+              include ${config.services.nginx.package}/conf/fastcgi_params;
               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
               fastcgi_param REDIRECT_STATUS 200;
               fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index 1d0a620585b0b..38c3392014f5c 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -164,6 +164,35 @@ in
             optional (cfg.dataDir == defaultdataDir) "galene/data" ++
             optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
             optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
+
+          # Hardening
+          CapabilityBoundingSet = [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          ReadWritePaths = cfg.recordingsDir;
+          RemoveIPC = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
         }
       ];
     };
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
index be2de638dd961..a77fddf1f2f45 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.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;
+      # PHP 8.0 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v3.3.0/README.md#how-to-install
+      phpPackage = pkgs.php80;
 
       inherit (cfg.phpfpm) settings;
 
diff --git a/nixos/modules/services/web-apps/healthchecks.nix b/nixos/modules/services/web-apps/healthchecks.nix
new file mode 100644
index 0000000000000..be025e7dd5518
--- /dev/null
+++ b/nixos/modules/services/web-apps/healthchecks.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+  defaultUser = "healthchecks";
+  cfg = config.services.healthchecks;
+  pkg = cfg.package;
+  boolToPython = b: if b then "True" else "False";
+  environment = {
+    PYTHONPATH = pkg.pythonPath;
+    STATIC_ROOT = cfg.dataDir + "/static";
+    DB_NAME = "${cfg.dataDir}/healthchecks.sqlite";
+  } // cfg.settings;
+
+  environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment);
+
+  healthchecksManageScript = with pkgs; (writeShellScriptBin "healthchecks-manage" ''
+    if [[ "$USER" != "${cfg.user}" ]]; then
+        echo "please run as user 'healtchecks'." >/dev/stderr
+        exit 1
+    fi
+    export $(cat ${environmentFile} | xargs);
+    exec ${pkg}/opt/healthchecks/manage.py "$@"
+  '');
+in
+{
+  options.services.healthchecks = {
+    enable = mkEnableOption "healthchecks" // {
+      description = ''
+        Enable healthchecks.
+        It is expected to be run behind a HTTP reverse proxy.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.healthchecks;
+      defaultText = literalExpression "pkgs.healthchecks";
+      type = types.package;
+      description = "healthchecks package to use.";
+    };
+
+    user = mkOption {
+      default = defaultUser;
+      type = types.str;
+      description = ''
+        User account under which healthchecks runs.
+
+        <note><para>
+        If left as the default value this user will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the user exists before the healthchecks service starts.
+        </para></note>
+      '';
+    };
+
+    group = mkOption {
+      default = defaultUser;
+      type = types.str;
+      description = ''
+        Group account under which healthchecks runs.
+
+        <note><para>
+        If left as the default value this group will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the group exists before the healthchecks service starts.
+        </para></note>
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Address the server will listen on.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = "Port the server will listen on.";
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/healthchecks";
+      description = ''
+        The directory used to store all data for healthchecks.
+
+        <note><para>
+        If left as the default value this directory will automatically be created before
+        the healthchecks server starts, otherwise you are responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Environment variables which are read by healthchecks <literal>(local)_settings.py</literal>.
+
+        Settings which are explictly covered in options bewlow, are type-checked and/or transformed
+        before added to the environment, everything else is passed as a string.
+
+        See <link xlink:href="">https://healthchecks.io/docs/self_hosted_configuration/</link>
+        for a full documentation of settings.
+
+        We add two variables to this list inside the packages <literal>local_settings.py.</literal>
+        - STATIC_ROOT to set a state directory for dynamically generated static files.
+        - SECRET_KEY_FILE to read SECRET_KEY from a file at runtime and keep it out of /nix/store.
+      '';
+      type = types.submodule {
+        freeformType = types.attrsOf types.str;
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = types.listOf types.str;
+            default = [ "*" ];
+            description = "The host/domain names that this site can serve.";
+            apply = lib.concatStringsSep ",";
+          };
+
+          SECRET_KEY_FILE = mkOption {
+            type = types.path;
+            description = "Path to a file containing the secret key.";
+          };
+
+          DEBUG = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Enable debug mode.";
+            apply = boolToPython;
+          };
+
+          REGISTRATION_OPEN = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              A boolean that controls whether site visitors can create new accounts.
+              Set it to false if you are setting up a private Healthchecks instance,
+              but it needs to be publicly accessible (so, for example, your cloud
+              services can send pings to it).
+              If you close new user registration, you can still selectively invite
+              users to your team account.
+            '';
+            apply = boolToPython;
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ healthchecksManageScript ];
+
+    systemd.targets.healthchecks = {
+      description = "Target for all Healthchecks services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+    };
+
+    systemd.services =
+      let
+        commonConfig = {
+          WorkingDirectory = cfg.dataDir;
+          User = cfg.user;
+          Group = cfg.group;
+          EnvironmentFile = environmentFile;
+          StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
+          StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
+        };
+      in
+        {
+        healthchecks-migration = {
+          description = "Healthchecks migrations";
+          wantedBy = [ "healthchecks.target" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "on-failure";
+            Type = "oneshot";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py migrate
+            '';
+          };
+        };
+
+        healthchecks = {
+          description = "Healthchecks WSGI Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks-migration.service" ];
+
+          preStart = ''
+            ${pkg}/opt/healthchecks/manage.py collectstatic --no-input
+            ${pkg}/opt/healthchecks/manage.py remove_stale_contenttypes --no-input
+            ${pkg}/opt/healthchecks/manage.py compress
+          '';
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkgs.python3Packages.gunicorn}/bin/gunicorn hc.wsgi \
+                --bind ${cfg.listenAddress}:${toString cfg.port} \
+                --pythonpath ${pkg}/opt/healthchecks
+            '';
+          };
+        };
+
+        healthchecks-sendalerts = {
+          description = "Healthchecks Alert Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks.service" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py sendalerts
+            '';
+          };
+        };
+
+        healthchecks-sendreports = {
+          description = "Healthchecks Reporting Service";
+          wantedBy = [ "healthchecks.target" ];
+          after = [ "healthchecks.service" ];
+
+          serviceConfig = commonConfig // {
+            Restart = "always";
+            ExecStart = ''
+              ${pkg}/opt/healthchecks/manage.py sendreports --loop
+            '';
+          };
+        };
+      };
+
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        {
+          description = "healthchecks service owner";
+          isSystemUser = true;
+          group = defaultUser;
+        };
+    };
+
+    users.groups = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        {
+          members = [ defaultUser ];
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index 9eeabb9d5662b..f0e4f753eb70d 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -13,17 +13,22 @@ let
     then "hedgedoc"
     else "codimd";
 
+  settingsFormat = pkgs.formats.json {};
+
   prettyJSON = conf:
     pkgs.runCommandLocal "hedgedoc-config.json" {
       nativeBuildInputs = [ pkgs.jq ];
     } ''
-      echo '${builtins.toJSON conf}' | jq \
-        '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
+      jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \
+        < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \
+        > $out
     '';
 in
 {
   imports = [
     (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+    (mkRenamedOptionModule
+      [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
   ];
 
   options.services.hedgedoc = {
@@ -45,7 +50,7 @@ in
       '';
     };
 
-    configuration = {
+    settings = let options = {
       debug = mkEnableOption "debug mode";
       domain = mkOption {
         type = types.nullOr types.str;
@@ -197,6 +202,13 @@ in
           Whether to allow note creation by accessing a nonexistent note URL.
         '';
       };
+      requireFreeURLAuthentication = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to require authentication for FreeURL mode style note creation.
+        '';
+      };
       defaultPermission = mkOption {
         type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
         default = "editable";
@@ -431,7 +443,7 @@ in
                 Minio secret key.
               '';
             };
-            endpoint = mkOption {
+            endPoint = mkOption {
               type = types.str;
               description = ''
                 Minio endpoint.
@@ -831,7 +843,8 @@ in
               '';
             };
             searchAttributes = mkOption {
-              type = types.listOf types.str;
+              type = types.nullOr (types.listOf types.str);
+              default = null;
               example = [ "displayName" "mail" ];
               description = ''
                 LDAP attributes to search with.
@@ -854,6 +867,7 @@ in
             };
             tlsca = mkOption {
               type = types.str;
+              default = "/etc/ssl/certs/ca-certificates.crt";
               example = "server-cert.pem,root.pem";
               description = ''
                 Root CA for LDAP TLS in PEM format.
@@ -953,6 +967,16 @@ in
         default = null;
         description = "Configure the SAML integration.";
       };
+    }; in lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        inherit options;
+      };
+      description = ''
+        HedgeDoc configuration, see
+        <link xlink:href="https://docs.hedgedoc.org/configuration/"/>
+        for documentation.
+      '';
     };
 
     environmentFile = mkOption {
@@ -993,12 +1017,13 @@ in
         Package that provides HedgeDoc.
       '';
     };
+
   };
 
   config = mkIf cfg.enable {
     assertions = [
-      { assertion = cfg.configuration.db == {} -> (
-          cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
+      { assertion = cfg.settings.db == {} -> (
+          cfg.settings.dbURL != "" && cfg.settings.dbURL != null
         );
         message = "Database configuration for HedgeDoc missing."; }
     ];
@@ -1019,10 +1044,12 @@ in
       preStart = ''
         ${pkgs.envsubst}/bin/envsubst \
           -o ${cfg.workDir}/config.json \
-          -i ${prettyJSON cfg.configuration}
+          -i ${prettyJSON cfg.settings}
+        mkdir -p ${cfg.settings.uploadsPath}
       '';
       serviceConfig = {
         WorkingDirectory = cfg.workDir;
+        StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ];
         ExecStart = "${cfg.package}/bin/hedgedoc";
         EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
         Environment = [
diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix
index 095eec36dec38..527e248f65b0b 100644
--- a/nixos/modules/services/web-apps/invoiceplane.nix
+++ b/nixos/modules/services/web-apps/invoiceplane.nix
@@ -236,7 +236,7 @@ in
     };
 
     services.phpfpm = {
-      phpPackage = pkgs.php74;
+      phpPackage = pkgs.php81;
       pools = mapAttrs' (hostName: cfg: (
         nameValuePair "invoiceplane-${hostName}" {
           inherit user;
@@ -302,4 +302,3 @@ in
 
   ]);
 }
-
diff --git a/nixos/modules/services/web-apps/jirafeau.nix b/nixos/modules/services/web-apps/jirafeau.nix
index a95e2b4f82a91..328c61c8e6462 100644
--- a/nixos/modules/services/web-apps/jirafeau.nix
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -136,7 +136,7 @@ in
               '';
             locations = {
               "~ \\.php$".extraConfig = ''
-                include ${pkgs.nginx}/conf/fastcgi_params;
+                include ${config.services.nginx.package}/conf/fastcgi_params;
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_index index.php;
                 fastcgi_pass unix:${config.services.phpfpm.pools.jirafeau.socket};
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 2f1c4acec1e8b..8ad92706b0647 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -159,7 +159,7 @@ in
       '';
     };
 
-    caddy.enable = mkEnableOption "Whether to enablle caddy reverse proxy to expose jitsi-meet";
+    caddy.enable = mkEnableOption "Whether to enable caddy reverse proxy to expose jitsi-meet";
 
     prosody.enable = mkOption {
       type = bool;
@@ -253,9 +253,20 @@ in
         '';
       };
     };
-    systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
-      EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
-      SupplementaryGroups = [ "jitsi-meet" ];
+    systemd.services.prosody = mkIf cfg.prosody.enable {
+      preStart = let
+        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
+      in ''
+        ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
+        ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+        ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
+        ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
+        ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
+      '';
+      serviceConfig = {
+        EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
+        SupplementaryGroups = [ "jitsi-meet" ];
+      };
     };
 
     users.groups.jitsi-meet = {};
@@ -266,14 +277,12 @@ in
     systemd.services.jitsi-meet-init-secrets = {
       wantedBy = [ "multi-user.target" ];
       before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
-      path = [ config.services.prosody.package ];
       serviceConfig = {
         Type = "oneshot";
       };
 
       script = let
         secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
-        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
       in
       ''
         cd /var/lib/jitsi-meet
@@ -291,12 +300,6 @@ in
         chmod 640 secrets-env
       ''
       + optionalString cfg.prosody.enable ''
-        prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
-        prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
-        prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
-        prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
-        prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
-
         # generate self-signed certificates
         if [ ! -f /var/lib/jitsi-meet.crt ]; then
           ${getBin pkgs.openssl}/bin/openssl req \
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index a01f0049b2c72..a1855e1c1a791 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -4,20 +4,94 @@ let
   cfg = config.services.keycloak;
   opt = options.services.keycloak;
 
-  inherit (lib) types mkOption concatStringsSep mapAttrsToList
-    escapeShellArg recursiveUpdate optionalAttrs boolToString mkOrder
-    sort filterAttrs concatMapStringsSep concatStrings mkIf
-    optionalString optionals mkDefault literalExpression hasSuffix
-    foldl' isAttrs filter attrNames elem literalDocBook
-    maintainers;
-
-  inherit (builtins) match typeOf;
+  inherit (lib)
+    types
+    mkMerge
+    mkOption
+    mkChangedOptionModule
+    mkRenamedOptionModule
+    mkRemovedOptionModule
+    concatStringsSep
+    mapAttrsToList
+    escapeShellArg
+    mkIf
+    optionalString
+    optionals
+    mkDefault
+    literalExpression
+    isAttrs
+    literalDocBook
+    maintainers
+    catAttrs
+    collect
+    splitString
+    ;
+
+  inherit (builtins)
+    elem
+    typeOf
+    isInt
+    isString
+    hashString
+    isPath
+    ;
+
+  prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
 in
 {
+  imports =
+    [
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "bindAddress" ]
+        [ "services" "keycloak" "settings" "http-host" ])
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
+        [ "services" "keycloak" "settings" "hostname-strict-backchannel"])
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpPort" ]
+        [ "services" "keycloak" "settings" "http-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpPort))
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpsPort" ]
+        [ "services" "keycloak" "settings" "https-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpsPort))
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "frontendUrl" ]
+        ''
+          Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
+          NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
+                See its description for more information.
+        '')
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "extraConfig" ]
+        "Use `services.keycloak.settings' instead.")
+    ];
+
   options.services.keycloak =
     let
-      inherit (types) bool str nullOr attrsOf path enum anything
-        package port;
+      inherit (types)
+        bool
+        str
+        int
+        nullOr
+        attrsOf
+        oneOf
+        path
+        enum
+        package
+        port;
+
+      assertStringPath = optionName: value:
+        if isPath value then
+          throw ''
+            services.keycloak.${optionName}:
+              ${toString value}
+              is a Nix path, but should be a string, since Nix
+              paths are copied into the world-readable Nix store.
+          ''
+        else value;
     in
     {
       enable = mkOption {
@@ -30,89 +104,14 @@ in
         '';
       };
 
-      bindAddress = mkOption {
-        type = 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 = mkOption {
-        type = 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 = mkOption {
-        type = 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 = mkOption {
-        type = str;
-        apply = x:
-          if x == "" || hasSuffix "/" x then
-            x
-          else
-            x + "/";
-        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 = mkOption {
-        type = 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.
-        '';
-      };
-
       sslCertificate = mkOption {
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_cert";
+        apply = assertStringPath "sslCertificate";
         description = ''
           The path to a PEM formatted certificate to use for TLS/SSL
           connections.
-
-          This should be a string, not a Nix path, since Nix paths are
-          copied into the world-readable Nix store.
         '';
       };
 
@@ -120,20 +119,28 @@ in
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_key";
+        apply = assertStringPath "sslCertificateKey";
         description = ''
           The path to a PEM formatted private key to use for TLS/SSL
           connections.
+        '';
+      };
 
-          This should be a string, not a Nix path, since Nix paths are
-          copied into the world-readable Nix store.
+      plugins = lib.mkOption {
+        type = lib.types.listOf lib.types.path;
+        default = [ ];
+        description = ''
+          Keycloak plugin jar, ear files or derivations containing
+          them. Packaged plugins are available through
+          <literal>pkgs.keycloak.plugins</literal>.
         '';
       };
 
       database = {
         type = mkOption {
-          type = enum [ "mysql" "postgresql" ];
+          type = enum [ "mysql" "mariadb" "postgresql" ];
           default = "postgresql";
-          example = "mysql";
+          example = "mariadb";
           description = ''
             The type of database Keycloak should connect to.
           '';
@@ -151,6 +158,7 @@ in
           let
             dbPorts = {
               postgresql = 5432;
+              mariadb = 3306;
               mysql = 3306;
             };
           in
@@ -199,6 +207,21 @@ in
           '';
         };
 
+        name = mkOption {
+          type = str;
+          default = "keycloak";
+          description = ''
+            Database name to use when connecting to an external or
+            manually provisioned database; has no effect when a local
+            database is automatically provisioned.
+
+            To use this with a local database, set <xref
+            linkend="opt-services.keycloak.database.createLocally" /> to
+            <literal>false</literal> and create the database and user
+            manually.
+          '';
+        };
+
         username = mkOption {
           type = str;
           default = "keycloak";
@@ -210,19 +233,16 @@ in
             To use this with a local database, set <xref
             linkend="opt-services.keycloak.database.createLocally" /> to
             <literal>false</literal> and create the database and user
-            manually. The database should be called
-            <literal>keycloak</literal>.
+            manually.
           '';
         };
 
         passwordFile = mkOption {
           type = path;
           example = "/run/keys/db_password";
+          apply = assertStringPath "passwordFile";
           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.
+            The path to a file containing the database password.
           '';
         };
       };
@@ -260,67 +280,181 @@ in
         '';
       };
 
-      extraConfig = mkOption {
-        type = attrsOf anything;
-        default = { };
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
+
+          options = {
+            http-host = mkOption {
+              type = str;
+              default = "0.0.0.0";
+              example = "127.0.0.1";
+              description = ''
+                On which address Keycloak should accept new connections.
+              '';
+            };
+
+            http-port = mkOption {
+              type = port;
+              default = 80;
+              example = 8080;
+              description = ''
+                On which port Keycloak should listen for new HTTP connections.
+              '';
+            };
+
+            https-port = mkOption {
+              type = port;
+              default = 443;
+              example = 8443;
+              description = ''
+                On which port Keycloak should listen for new HTTPS connections.
+              '';
+            };
+
+            http-relative-path = mkOption {
+              type = str;
+              default = "";
+              example = "/auth";
+              description = ''
+                The path relative to <literal>/</literal> for serving
+                resources.
+
+                <note>
+                  <para>
+                    In versions of Keycloak using Wildfly (&lt;17),
+                    this defaulted to <literal>/auth</literal>. If
+                    upgrading from the Wildfly version of Keycloak,
+                    i.e. a NixOS version before 22.05, you'll likely
+                    want to set this to <literal>/auth</literal> to
+                    keep compatibility with your clients.
+
+                    See <link
+                    xlink:href="https://www.keycloak.org/migration/migrating-to-quarkus"
+                    /> for more information on migrating from Wildfly
+                    to Quarkus.
+                  </para>
+                </note>
+              '';
+            };
+
+            hostname = mkOption {
+              type = str;
+              example = "keycloak.example.com";
+              description = ''
+                The hostname part of the public URL used as base for
+                all frontend requests.
+
+                See <link xlink:href="https://www.keycloak.org/server/hostname" />
+                for more information about hostname configuration.
+              '';
+            };
+
+            hostname-strict-backchannel = mkOption {
+              type = bool;
+              default = false;
+              example = true;
+              description = ''
+                Whether Keycloak should force all requests to go
+                through the frontend URL. 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/server/hostname" />
+                for more information about hostname configuration.
+              '';
+            };
+
+            proxy = mkOption {
+              type = enum [ "edge" "reencrypt" "passthrough" "none" ];
+              default = "none";
+              example = "edge";
+              description = ''
+                The proxy address forwarding mode if the server is
+                behind a reverse proxy.
+
+                <variablelist>
+                  <varlistentry>
+                    <term>edge</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>reencrypt</term>
+                    <listitem>
+                      <para>
+                        Requires communication through HTTPS between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>passthrough</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP or HTTPS between
+                        the proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                </variablelist>
+
+                See <link
+                xlink:href="https://www.keycloak.org/server/reverseproxy"
+                /> for more information.
+              '';
+            };
+          };
+        };
+
         example = literalExpression ''
           {
-            "subsystem=keycloak-server" = {
-              "spi=hostname" = {
-                "provider=default" = null;
-                "provider=fixed" = {
-                  enabled = true;
-                  properties.hostname = "keycloak.example.com";
-                };
-                default-provider = "fixed";
-              };
-            };
+            hostname = "keycloak.example.com";
+            proxy = "reencrypt";
+            https-key-store-file = "/path/to/file";
+            https-key-store-password = { _secret = "/run/keys/store_password"; };
           }
         '';
+
         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>.
+          Configuration options corresponding to parameters set in
+          <filename>conf/keycloak.conf</filename>.
+
+          Most available options are documented at <link
+          xlink:href="https://www.keycloak.org/server/all-config" />.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>conf/keycloak.conf</filename> file, the
+          <literal>https-key-store-password</literal> key will be set
+          to the contents of the
+          <filename>/run/keys/store_password</filename> file.
         '';
       };
-
     };
 
   config =
     let
-      # We only want to create a database if we're actually going to connect to it.
+      # We only want to create a database if we're actually going to
+      # connect to it.
       databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
       createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
-      createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql";
+      createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
 
       mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
         ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
       '';
 
-      # Both theme and theme type directories need to be actual directories in one hierarchy to pass Keycloak checks.
+      # Both theme and theme type directories need to be actual
+      # directories in one hierarchy to pass Keycloak checks.
       themesBundle = pkgs.runCommand "keycloak-themes" { } ''
         linkTheme() {
           theme="$1"
@@ -339,7 +473,7 @@ in
         }
 
         mkdir -p "$out"
-        for theme in ${cfg.package}/themes/*; do
+        for theme in ${keycloakBuild}/themes/*; do
           if [ -d "$theme" ]; then
             linkTheme "$theme" "$(basename "$theme")"
           fi
@@ -348,329 +482,25 @@ in
         ${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
       '';
 
-      keycloakConfig' = foldl' 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;
-              };
-            };
-            "theme=defaults".dir = toString themesBundle;
-          };
-          "subsystem=datasources"."data-source=KeycloakDS" = {
-            max-pool-size = "20";
-            user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
-            password = "@db-password@";
-          };
-        } [
-        (optionalAttrs (cfg.database.type == "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.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "postgresql";
-              "connection-properties=ssl".value = boolToString cfg.database.useSSL;
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=sslrootcert".value = cfg.database.caCert;
-              "connection-properties=sslmode".value = "verify-ca";
-            });
-          };
-        })
-        (optionalAttrs (cfg.database.type == "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.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "mysql";
-              "connection-properties=useSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=requireSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=verifyServerCertificate".value = boolToString cfg.database.useSSL;
-              "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";
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
-              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
-            });
-          };
-        })
-        (optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
-          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
-          "subsystem=elytron" = mkOrder 900 {
-            "key-store=httpsKS" = mkOrder 900 {
-              path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
-              credential-reference.clear-text = "notsosecretpassword";
-              type = "JKS";
-            };
-            "key-manager=httpsKM" = mkOrder 901 {
-              key-store = "httpsKS";
-              credential-reference.clear-text = "notsosecretpassword";
-            };
-            "server-ssl-context=httpsSSC" = mkOrder 902 {
-              key-manager = "httpsKM";
-            };
-          };
-          "subsystem=undertow" = mkOrder 901 {
-            "server=default-server"."https-listener=https".ssl-context = "httpsSSC";
-          };
-        })
-        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
-                  matchResult = match ''"\$\{.*}"'' string;
-                in
-                if matchResult != null then
-                  "expression " + string
-                else
-                  string;
-
-              writeAttribute = attribute: value:
-                let
-                  type = typeOf value;
-                in
-                if type == "set" then
-                  let
-                    names = attrNames value;
-                  in
-                  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 elem type [ "string" "path" "bool" ] then
-                  let
-                    value' = if type == "bool" then 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
-            concatStrings
-              (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 = typeOf value;
-                in
-                if type == "set" then
-                  "${attribute} = { " + (makeArgList value) + " }"
-                else if elem type [ "string" "path" "bool" ] then
-                  "${attribute} = ${if type == "bool" then boolToString value else ''"${value}"''}"
-                else if value == null then
-                  ""
-                else
-                  throw "Unsupported type '${type}' for attribute '${attribute}'!";
-
-            in
-            concatStringsSep ", " (mapAttrsToList makeArg set);
-
-
-          /* Recurses into the `nodeValue` attrset. Only subattrsets that
-             are JBoss paths, i.e. follows the `key=value` format, are recursed
-             into - the rest are considered JBoss attributes / maps.
-          */
-          recurse = nodePath: nodeValue:
-            let
-              nodeContent =
-                if isAttrs nodeValue && nodeValue._type or "" == "order" then
-                  nodeValue.content
-                else
-                  nodeValue;
-              isPath = name:
-                let
-                  value = nodeContent.${name};
-                in
-                if (match ".*([=]).*" name) == [ "=" ] then
-                  if isAttrs value || value == null then
-                    true
-                  else
-                    throw "Parsing path '${concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
-                else
-                  false;
-              jbossPath = "/" + concatStringsSep "/" nodePath;
-              children = if !isAttrs nodeContent then { } else nodeContent;
-              subPaths = filter isPath (attrNames children);
-              getPriority = name:
-                let
-                  value = children.${name};
-                in
-                if value._type or "" == "order" then value.priority else 1000;
-              orderedSubPaths = sort (a: b: getPriority a < getPriority b) subPaths;
-              jbossAttrs = filterAttrs (name: _: !(isPath name)) children;
-              text =
-                if nodeContent != 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
-                  '';
-            in
-            text + concatMapStringsSep "\n" (name: recurse (nodePath ++ [ name ]) children.${name}) orderedSubPaths;
-        in
-        recurse [ ] attrs;
-
-      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
-
-      keycloakConfig = pkgs.runCommand "keycloak-config"
-        {
-          nativeBuildInputs = [ cfg.package ];
-        }
-        ''
-          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}
-
-          standalone.sh&
-
-          attempt=1
-          max_attempts=30
-          while ! 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
-
-          jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+      keycloakConfig = lib.generators.toKeyValue {
+        mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+          mkValueString = v: with builtins;
+            if isInt v then toString v
+            else if isString v then v
+            else if true == v then "true"
+            else if false == v then "false"
+            else if isSecret v then hashString "sha256" v._secret
+            else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+        };
+      };
 
-          cp configuration/standalone.xml $out
-        '';
+      isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+      filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
+      confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
+      keycloakBuild = cfg.package.override {
+        inherit confFile;
+        plugins = cfg.package.enabledPlugins ++ cfg.plugins;
+      };
     in
     mkIf cfg.enable
       {
@@ -681,7 +511,46 @@ in
           }
         ];
 
-        environment.systemPackages = [ cfg.package ];
+        environment.systemPackages = [ keycloakBuild ];
+
+        services.keycloak.settings =
+          let
+            postgresParams = concatStringsSep "&" (
+              optionals cfg.database.useSSL [
+                "ssl=true"
+              ] ++ optionals (cfg.database.caCert != null) [
+                "sslrootcert=${cfg.database.caCert}"
+                "sslmode=verify-ca"
+              ]
+            );
+            mariadbParams = concatStringsSep "&" ([
+              "characterEncoding=UTF-8"
+            ] ++ optionals cfg.database.useSSL [
+              "useSSL=true"
+              "requireSSL=true"
+              "verifyServerCertificate=true"
+            ] ++ optionals (cfg.database.caCert != null) [
+              "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
+              "trustCertificateKeyStorePassword=notsosecretpassword"
+            ]);
+            dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
+          in
+          mkMerge [
+            {
+              db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
+              db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
+              db-password._secret = cfg.database.passwordFile;
+              db-url-host = cfg.database.host;
+              db-url-port = toString cfg.database.port;
+              db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
+              db-url-properties = prefixUnlessEmpty "?" dbProps;
+              db-url = null;
+            }
+            (mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
+              https-certificate-file = "/run/keycloak/ssl/ssl_cert";
+              https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
+            })
+          ];
 
         systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
           after = [ "postgresql.service" ];
@@ -693,6 +562,7 @@ in
             RemainAfterExit = true;
             User = "postgres";
             Group = "postgres";
+            LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
           };
           script = ''
             set -o errexit -o pipefail -o nounset -o errtrace
@@ -701,7 +571,8 @@ in
             create_role="$(mktemp)"
             trap 'rm -f "$create_role"' ERR EXIT
 
-            echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.database.passwordFile}')' CREATEDB" > "$create_role"
+            db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
+            echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
             psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
             psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
           '';
@@ -717,14 +588,14 @@ in
             RemainAfterExit = true;
             User = config.services.mysql.user;
             Group = config.services.mysql.group;
+            LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
           };
           script = ''
             set -o errexit -o pipefail -o nounset -o errtrace
             shopt -s inherit_errexit
-
-            db_password="$(<'${cfg.database.passwordFile}')"
+            db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
             ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
-              echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
+              echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
               echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
             ) | mysql -N
           '';
@@ -742,41 +613,37 @@ in
                 "mysql.service"
               ]
               else [ ];
+            secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
+            mkSecretReplacement = file: ''
+              replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
+            '';
+            secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
           in
           {
             after = databaseServices;
             bindsTo = databaseServices;
             wantedBy = [ "multi-user.target" ];
             path = with pkgs; [
-              cfg.package
+              keycloakBuild
               openssl
               replace-secret
             ];
             environment = {
-              JBOSS_LOG_DIR = "/var/log/keycloak";
-              JBOSS_BASE_DIR = "/run/keycloak";
-              JBOSS_MODULEPATH = "${cfg.package}/modules";
+              KC_HOME_DIR = "/run/keycloak";
+              KC_CONF_DIR = "/run/keycloak/conf";
             };
             serviceConfig = {
-              LoadCredential = [
-                "db_password:${cfg.database.passwordFile}"
-              ] ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
-                "ssl_cert:${cfg.sslCertificate}"
-                "ssl_key:${cfg.sslCertificateKey}"
-              ];
+              LoadCredential =
+                map (p: "${baseNameOf p}:${p}") secretPaths
+                ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
+                  "ssl_cert:${cfg.sslCertificate}"
+                  "ssl_key:${cfg.sslCertificateKey}"
+                ];
               User = "keycloak";
               Group = "keycloak";
               DynamicUser = true;
-              RuntimeDirectory = map (p: "keycloak/" + p) [
-                "configuration"
-                "deployments"
-                "data"
-                "ssl"
-                "log"
-                "tmp"
-              ];
+              RuntimeDirectory = "keycloak";
               RuntimeDirectoryMode = 0700;
-              LogsDirectory = "keycloak";
               AmbientCapabilities = "CAP_NET_BIND_SERVICE";
             };
             script = ''
@@ -785,31 +652,30 @@ in
 
               umask u=rwx,g=,o=
 
-              install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
-              install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
+              ln -s ${themesBundle} /run/keycloak/themes
+              ln -s ${keycloakBuild}/providers /run/keycloak/
 
-              replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml
+              install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
+
+              ${secretReplacements}
 
-              export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
-              add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
             '' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
-              pushd /run/keycloak/ssl/
-              cat "$CREDENTIALS_DIRECTORY/ssl_cert" <(echo) \
-                  "$CREDENTIALS_DIRECTORY/ssl_key" <(echo) \
-                  /etc/ssl/certs/ca-certificates.crt \
-                  > allcerts.pem
-              openssl pkcs12 -export -in "$CREDENTIALS_DIRECTORY/ssl_cert" -inkey "$CREDENTIALS_DIRECTORY/ssl_key" -chain \
-                             -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
-                             -CAfile allcerts.pem -passout pass:notsosecretpassword
-              popd
+              mkdir -p /run/keycloak/ssl
+              cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
             '' + ''
-              ${cfg.package}/bin/standalone.sh
+              export KEYCLOAK_ADMIN=admin
+              export KEYCLOAK_ADMIN_PASSWORD=${cfg.initialAdminPassword}
+              kc.sh start
             '';
           };
 
         services.postgresql.enable = mkDefault createLocalPostgreSQL;
         services.mysql.enable = mkDefault createLocalMySQL;
-        services.mysql.package = mkIf createLocalMySQL pkgs.mariadb;
+        services.mysql.package =
+          let
+            dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
+          in
+          mkIf createLocalMySQL (mkDefault dbPkg);
       };
 
   meta.doc = ./keycloak.xml;
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
index cb706932f48f5..861756e33ac09 100644
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -27,10 +27,10 @@
 
      <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.
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
+       Keycloak Server Administration Guide</link> for information on
+       how to administer your <productname>Keycloak</productname>
+       instance.
      </para>
    </section>
 
@@ -38,27 +38,28 @@
      <title>Database access</title>
      <para>
        <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname> or
+       <productname>PostgreSQL</productname>,
+       <productname>MariaDB</productname> or
        <productname>MySQL</productname>. Which one is used can be
        configured in <xref
        linkend="opt-services.keycloak.database.type" />. The selected
        database will automatically be enabled and a database and role
        created unless <xref
-       linkend="opt-services.keycloak.database.host" /> is changed from
-       its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.database.createLocally" /> is set
-       to <literal>false</literal>.
+       linkend="opt-services.keycloak.database.host" /> is changed
+       from its default of <literal>localhost</literal> or <xref
+       linkend="opt-services.keycloak.database.createLocally" /> is
+       set to <literal>false</literal>.
      </para>
 
      <para>
        External database access can also be configured by setting
        <xref linkend="opt-services.keycloak.database.host" />, <xref
+       linkend="opt-services.keycloak.database.name" />, <xref
        linkend="opt-services.keycloak.database.username" />, <xref
        linkend="opt-services.keycloak.database.useSSL" /> and <xref
        linkend="opt-services.keycloak.database.caCert" /> 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.
+       appropriate. Note that you need to manually create the database
+       and allow the configured database user full access to it.
      </para>
 
      <para>
@@ -79,22 +80,27 @@
      </warning>
    </section>
 
-   <section xml:id="module-services-keycloak-frontendurl">
-     <title>Frontend URL</title>
+   <section xml:id="module-services-keycloak-hostname">
+     <title>Hostname</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). If you use a reverse proxy, you need
-       to set this option to <literal>""</literal>, so that frontend URL
-       is derived from HTTP headers. <literal>X-Forwarded-*</literal> headers
-       support also should be enabled, using <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html#identifying-client-ip-addresses">
-       respective guidelines</link>.
+       The hostname is used to build the public URL used as base for
+       all frontend requests and must be configured through <xref
+       linkend="opt-services.keycloak.settings.hostname" />.
      </para>
 
+     <note>
+       <para>
+         If you're migrating an old Wildfly based Keycloak instance
+         and want to keep compatibility with your current clients,
+         you'll likely want to set <xref
+         linkend="opt-services.keycloak.settings.http-relative-path"
+         /> to <literal>/auth</literal>. See the option description
+         for more details.
+       </para>
+     </note>
+
      <para>
-       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
        determines whether Keycloak should force all requests to go
        through the frontend URL. By default,
        <productname>Keycloak</productname> allows backend requests to
@@ -104,10 +110,10 @@
      </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.
+        For more information on hostname configuration, see the <link
+        xlink:href="https://www.keycloak.org/server/hostname">Hostname
+        section of the Keycloak Server Installation and Configuration
+        Guide</link>.
      </para>
    </section>
 
@@ -139,68 +145,40 @@
    <section xml:id="module-services-keycloak-themes">
      <title>Themes</title>
      <para>
-        You can package custom themes and make them visible to Keycloak via
-        <xref linkend="opt-services.keycloak.themes" />
-        option. See the <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
+        You can package custom themes and make them visible to
+        Keycloak through <xref linkend="opt-services.keycloak.themes"
+        />. See the <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
         Themes section of the Keycloak Server Development Guide</link>
-        and respective NixOS option description for more information.
+        and the description of the aforementioned NixOS option for
+        more information.
      </para>
    </section>
 
-   <section xml:id="module-services-keycloak-extra-config">
-     <title>Additional configuration</title>
+   <section xml:id="module-services-keycloak-settings">
+     <title>Configuration file settings</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" />.
+       Keycloak server configuration parameters can be set in <xref
+       linkend="opt-services.keycloak.settings" />. These correspond
+       directly to options in
+       <filename>conf/keycloak.conf</filename>. Some of the most
+       important parameters are documented as suboptions, the rest can
+       be found in the <link
+       xlink:href="https://www.keycloak.org/server/all-config">All
+       configuration section of the Keycloak Server Installation and
+       Configuration Guide</link>.
      </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>.
+       Options containing secret data should be set to an attribute
+       set containing the attribute <literal>_secret</literal> - a
+       string pointing to a file containing the value the option
+       should be set to. See the description of <xref
+       linkend="opt-services.keycloak.settings" /> for an example.
      </para>
    </section>
 
+
    <section xml:id="module-services-keycloak-example-config">
      <title>Example configuration</title>
      <para>
@@ -208,9 +186,11 @@ services.keycloak.extraConfig = {
 <programlisting>
 services.keycloak = {
   <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  settings = {
+    <link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
+    <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</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.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
   <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
   <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
index 7cd2357c45566..15e616c5d5ec1 100644
--- a/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -164,7 +164,7 @@ in
 
         wantedBy = [ "multi-user.target" ];
 
-        after = [ "pict-rs.service " ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
+        after = [ "pict-rs.service" ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
 
         requires = lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
 
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index 1e3c7e53c175a..03adaadff93d0 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -9,6 +9,8 @@ let
     RAILS_ENV = "production";
     NODE_ENV = "production";
 
+    LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+
     # mastodon-web concurrency.
     WEB_CONCURRENCY = toString cfg.webProcesses;
     MAX_THREADS = toString cfg.webThreads;
@@ -92,6 +94,7 @@ let
 
   mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
     set -a
+    export RAILS_ROOT="${cfg.package}"
     source "${envFile}"
     source /var/lib/mastodon/.secrets_env
     eval -- "\$@"
@@ -120,7 +123,7 @@ in {
 
           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>.
+          <code>https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf</code>.
         '';
         type = lib.types.bool;
         default = false;
@@ -291,7 +294,7 @@ in {
         port = lib.mkOption {
           description = "Redis port.";
           type = lib.types.port;
-          default = 6379;
+          default = 31637;
         };
       };
 
@@ -602,8 +605,10 @@ in {
       enable = true;
       hostname = lib.mkDefault "${cfg.localDomain}";
     };
-    services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
+    services.redis.servers.mastodon = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
       enable = true;
+      port = cfg.redis.port;
+      bind = "127.0.0.1";
     };
     services.postgresql = lib.mkIf databaseActuallyCreateLocally {
       enable = true;
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 8a0ca33b51f03..c6d4ed6d39dec 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -192,6 +192,7 @@ in {
             # Copy config folder
             chmod g+s "${dataDir}"
             cp -r "${cfg.package}/share/config" "${dataDir}/"
+            mkdir -p "${dataDir}/misc"
             chmod -R u+rwX,g+rwX,o-rwx "${dataDir}"
 
             # check whether user setup has already been done
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index 14cbfb3954028..641c9be85d8cb 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -7,26 +7,12 @@ let
   defaultAddress = "localhost:8080";
 
   dbUser = "miniflux";
-  dbPassword = "miniflux";
-  dbHost = "localhost";
   dbName = "miniflux";
 
-  defaultCredentials = pkgs.writeText "miniflux-admin-credentials" ''
-    ADMIN_USERNAME=admin
-    ADMIN_PASSWORD=password
-  '';
-
   pgbin = "${config.services.postgresql.package}/bin";
   preStart = pkgs.writeScript "miniflux-pre-start" ''
     #!${pkgs.runtimeShell}
-    db_exists() {
-      [ "$(${pgbin}/psql -Atc "select 1 from pg_database where datname='$1'")" == "1" ]
-    }
-    if ! db_exists "${dbName}"; then
-      ${pgbin}/psql postgres -c "CREATE ROLE ${dbUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${dbPassword}'"
-      ${pgbin}/createdb --owner "${dbUser}" "${dbName}"
-      ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
-    fi
+    ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
   '';
 in
 
@@ -54,11 +40,10 @@ in
       };
 
       adminCredentialsFile = mkOption  {
-        type = types.nullOr types.path;
-        default = null;
+        type = types.path;
         description = ''
-          File containing the ADMIN_USERNAME, default is "admin", and
-          ADMIN_PASSWORD (length >= 6), default is "password"; in the format of
+          File containing the ADMIN_USERNAME and
+          ADMIN_PASSWORD (length >= 6) in the format of
           an EnvironmentFile=, as described by systemd.exec(5).
         '';
         example = "/etc/nixos/miniflux-admin-credentials";
@@ -70,16 +55,24 @@ in
 
     services.miniflux.config =  {
       LISTEN_ADDR = mkDefault defaultAddress;
-      DATABASE_URL = "postgresql://${dbUser}:${dbPassword}@${dbHost}/${dbName}?sslmode=disable";
+      DATABASE_URL = "user=${dbUser} host=/run/postgresql dbname=${dbName}";
       RUN_MIGRATIONS = "1";
       CREATE_ADMIN = "1";
     };
 
-    services.postgresql.enable = true;
+    services.postgresql = {
+      enable = true;
+      ensureUsers = [ {
+        name = dbUser;
+        ensurePermissions = {
+          "DATABASE ${dbName}" = "ALL PRIVILEGES";
+        };
+      } ];
+      ensureDatabases = [ dbName ];
+    };
 
     systemd.services.miniflux-dbsetup = {
       description = "Miniflux database setup";
-      wantedBy = [ "multi-user.target" ];
       requires = [ "postgresql.service" ];
       after = [ "network.target" "postgresql.service" ];
       serviceConfig = {
@@ -92,17 +85,16 @@ in
     systemd.services.miniflux = {
       description = "Miniflux service";
       wantedBy = [ "multi-user.target" ];
-      requires = [ "postgresql.service" ];
+      requires = [ "miniflux-dbsetup.service" ];
       after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
 
       serviceConfig = {
         ExecStart = "${pkgs.miniflux}/bin/miniflux";
+        User = dbUser;
         DynamicUser = true;
         RuntimeDirectory = "miniflux";
         RuntimeDirectoryMode = "0700";
-        EnvironmentFile = if cfg.adminCredentialsFile == null
-        then defaultCredentials
-        else cfg.adminCredentialsFile;
+        EnvironmentFile = cfg.adminCredentialsFile;
         # Hardening
         CapabilityBoundingSet = [ "" ];
         DeviceAllow = [ "" ];
@@ -119,7 +111,7 @@ in
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectProc = "invisible";
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
         RestrictNamespaces = true;
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 19f3e754691e0..55e5ac9281e7b 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -56,7 +56,7 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
-  phpExt = pkgs.php74.withExtensions
+  phpExt = pkgs.php81.withExtensions
         ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache ]);
 in
 {
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
new file mode 100644
index 0000000000000..a7d8bede74b4b
--- /dev/null
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+  cfg = config.services.netbox;
+  staticDir = cfg.dataDir + "/static";
+  configFile = pkgs.writeTextFile {
+    name = "configuration.py";
+    text = ''
+      STATIC_ROOT = '${staticDir}'
+      ALLOWED_HOSTS = ['*']
+      DATABASE = {
+        'NAME': 'netbox',
+        'USER': 'netbox',
+        'HOST': '/run/postgresql',
+      }
+
+      # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
+      # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
+      # to use two separate database IDs.
+      REDIS = {
+          'tasks': {
+              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=0',
+              'SSL': False,
+          },
+          'caching': {
+              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=1',
+              'SSL': False,
+          }
+      }
+
+      with open("${cfg.secretKeyFile}", "r") as file:
+          SECRET_KEY = file.readline()
+
+      ${optionalString cfg.enableLdap "REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'"}
+
+      ${cfg.extraConfig}
+    '';
+  };
+  pkg = (pkgs.netbox.overrideAttrs (old: {
+    installPhase = old.installPhase + ''
+      ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
+    '' + optionalString cfg.enableLdap ''
+      ln -s ${ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
+    '';
+  })).override {
+    plugins = ps: ((cfg.plugins ps)
+      ++ optional cfg.enableLdap [ ps.django-auth-ldap ]);
+  };
+  netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" ''
+    #!${stdenv.shell}
+    export PYTHONPATH=${pkg.pythonPath}
+    sudo -u netbox ${pkg}/bin/netbox "$@"
+  '');
+
+in {
+  options.services.netbox = {
+    enable = mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enable Netbox.
+
+        This module requires a reverse proxy that serves <literal>/static</literal> separately.
+        See this <link xlink:href="https://github.com/netbox-community/netbox/blob/develop/contrib/nginx.conf/">example</link> on how to configure this.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "[::1]";
+      description = ''
+        Address the server will listen on.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8001;
+      description = ''
+        Port the server will listen on.
+      '';
+    };
+
+    plugins = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = _: [];
+      defaultText = literalExpression ''
+        python3Packages: with python3Packages; [];
+      '';
+      description = ''
+        List of plugin packages to install.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/netbox";
+      description = ''
+        Storage path of netbox.
+      '';
+    };
+
+    secretKeyFile = mkOption {
+      type = types.path;
+      description = ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Additional lines of configuration appended to the <literal>configuration.py</literal>.
+        See the <link xlink:href="https://netbox.readthedocs.io/en/stable/configuration/optional-settings/">documentation</link> for more possible options.
+      '';
+    };
+
+    enableLdap = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable LDAP-Authentication for Netbox.
+
+        This requires a configuration file being pass through <literal>ldapConfigPath</literal>.
+      '';
+    };
+
+    ldapConfigPath = mkOption {
+      type = types.path;
+      default = "";
+      description = ''
+        Path to the Configuration-File for LDAP-Authentification, will be loaded as <literal>ldap_config.py</literal>.
+        See the <link xlink:href="https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration">documentation</link> for possible options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.redis.servers.netbox.enable = true;
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "netbox" ];
+      ensureUsers = [
+        {
+          name = "netbox";
+          ensurePermissions = {
+            "DATABASE netbox" = "ALL PRIVILEGES";
+          };
+        }
+      ];
+    };
+
+    environment.systemPackages = [ netboxManageScript ];
+
+    systemd.targets.netbox = {
+      description = "Target for all NetBox services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "redis-netbox.service" ];
+    };
+
+    systemd.services = let
+      defaultServiceConfig = {
+        WorkingDirectory = "${cfg.dataDir}";
+        User = "netbox";
+        Group = "netbox";
+        StateDirectory = "netbox";
+        StateDirectoryMode = "0750";
+        Restart = "on-failure";
+      };
+    in {
+      netbox-migration = {
+        description = "NetBox migrations";
+        wantedBy = [ "netbox.target" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          Type = "oneshot";
+          ExecStart = ''
+            ${pkg}/bin/netbox migrate
+          '';
+        };
+      };
+
+      netbox = {
+        description = "NetBox WSGI Service";
+        wantedBy = [ "netbox.target" ];
+        after = [ "netbox-migration.service" ];
+
+        preStart = ''
+          ${pkg}/bin/netbox trace_paths --no-input
+          ${pkg}/bin/netbox collectstatic --no-input
+          ${pkg}/bin/netbox remove_stale_contenttypes --no-input
+        '';
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          ExecStart = ''
+            ${pkgs.python3Packages.gunicorn}/bin/gunicorn netbox.wsgi \
+              --bind ${cfg.listenAddress}:${toString cfg.port} \
+              --pythonpath ${pkg}/opt/netbox/netbox
+          '';
+        };
+      };
+
+      netbox-rq = {
+        description = "NetBox Request Queue Worker";
+        wantedBy = [ "netbox.target" ];
+        after = [ "netbox.service" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          ExecStart = ''
+            ${pkg}/bin/netbox rqworker high default low
+          '';
+        };
+      };
+
+      netbox-housekeeping = {
+        description = "NetBox housekeeping job";
+        after = [ "netbox.service" ];
+
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+
+        serviceConfig = defaultServiceConfig // {
+          Type = "oneshot";
+          ExecStart = ''
+            ${pkg}/bin/netbox housekeeping
+          '';
+        };
+      };
+    };
+
+    systemd.timers.netbox-housekeeping = {
+      description = "Run NetBox housekeeping job";
+      wantedBy = [ "timers.target" ];
+
+      timerConfig = {
+        OnCalendar = "daily";
+      };
+    };
+
+    users.users.netbox = {
+      home = "${cfg.dataDir}";
+      isSystemUser = true;
+      group = "netbox";
+    };
+    users.groups.netbox = {};
+    users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 141ab98e29bfc..f8082d7544c12 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -6,6 +6,8 @@ let
   cfg = config.services.nextcloud;
   fpm = config.services.phpfpm.pools.nextcloud;
 
+  jsonFormat = pkgs.formats.json {};
+
   inherit (cfg) datadir;
 
   phpPackage = cfg.phpPackage.buildEnv {
@@ -89,7 +91,8 @@ in {
     };
     datadir = mkOption {
       type = types.str;
-      defaultText = "config.services.nextcloud.home";
+      default = config.services.nextcloud.home;
+      defaultText = literalExpression "config.services.nextcloud.home";
       description = ''
         Data storage path of nextcloud.  Will be <xref linkend="opt-services.nextcloud.home" /> by default.
         This folder will be populated with a config.php and data folder which contains the state of the instance (excl the database).";
@@ -153,11 +156,11 @@ in {
     package = mkOption {
       type = types.package;
       description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud21" "nextcloud22" "nextcloud23" ];
+      relatedPackages = [ "nextcloud23" "nextcloud24" ];
     };
     phpPackage = mkOption {
       type = types.package;
-      relatedPackages = [ "php74" "php80" ];
+      relatedPackages = [ "php80" "php81" ];
       defaultText = "pkgs.php";
       description = ''
         PHP package to use for Nextcloud.
@@ -251,6 +254,23 @@ in {
       '';
     };
 
+    database = {
+
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Create the database and database user locally. Only available for
+          mysql database.
+          Note that this option will use the latest version of MariaDB which
+          is not officially supported by Nextcloud. As for now a workaround
+          is used to also support MariaDB version >= 10.6.
+        '';
+      };
+
+    };
+
+
     config = {
       dbtype = mkOption {
         type = types.enum [ "sqlite" "pgsql" "mysql" ];
@@ -505,17 +525,80 @@ in {
         The nextcloud-occ program preconfigured to target this Nextcloud instance.
       '';
     };
+    globalProfiles = mkEnableOption "global profiles" // {
+      description = ''
+        Makes user-profiles globally available under <literal>nextcloud.tld/u/user.name</literal>.
+        Even though it's enabled by default in Nextcloud, it must be explicitly enabled
+        here because it has the side-effect that personal information is even accessible to
+        unauthenticated users by default.
+
+        By default, the following properties are set to <quote>Show to everyone</quote>
+        if this flag is enabled:
+        <itemizedlist>
+        <listitem><para>About</para></listitem>
+        <listitem><para>Full name</para></listitem>
+        <listitem><para>Headline</para></listitem>
+        <listitem><para>Organisation</para></listitem>
+        <listitem><para>Profile picture</para></listitem>
+        <listitem><para>Role</para></listitem>
+        <listitem><para>Twitter</para></listitem>
+        <listitem><para>Website</para></listitem>
+        </itemizedlist>
+
+        Only has an effect in Nextcloud 23 and later.
+      '';
+    };
 
-    nginx.recommendedHttpHeaders = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Enable additional recommended HTTP response headers";
+    extraOptions = mkOption {
+      type = jsonFormat.type;
+      default = {};
+      description = ''
+        Extra options which should be appended to nextcloud's config.php file.
+      '';
+      example = literalExpression '' {
+        redis = {
+          host = "/run/redis/redis.sock";
+          port = 0;
+          dbindex = 0;
+          password = "secret";
+          timeout = 1.5;
+        };
+      } '';
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Secret options which will be appended to nextcloud's config.php file (written as JSON, in the same
+        form as the <xref linkend="opt-services.nextcloud.extraOptions"/> option), for example
+        <programlisting>{"redis":{"password":"secret"}}</programlisting>.
+      '';
+    };
+
+    nginx = {
+      recommendedHttpHeaders = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Enable additional recommended HTTP response headers";
+      };
+      hstsMaxAge = mkOption {
+        type = types.ints.positive;
+        default = 15552000;
+        description = ''
+          Value for the <code>max-age</code> directive of the HTTP
+          <code>Strict-Transport-Security</code> header.
+
+          See section 6.1.1 of IETF RFC 6797 for detailed information on this
+          directive and header.
+        '';
+      };
     };
   };
 
   config = mkIf cfg.enable (mkMerge [
     { warnings = let
-        latest = 23;
+        latest = 24;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -551,6 +634,7 @@ in {
         ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
         ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"))
         ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
+        ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05"))
         ++ (optional isUnsupportedMariadb ''
             You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)!
             Please note that this isn't supported officially by Nextcloud. You can either
@@ -571,29 +655,30 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          # 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.
-          # FIXME(@Ma27) remove this else-if as soon as 21.05 is EOL! This is only here
-          # to ensure that users who are on Nextcloud 19 with a stateVersion <21.05 with
-          # no explicit services.nextcloud.package don't upgrade to v21 by accident (
-          # nextcloud20 throws an eval-error because it's dropped).
-          else if versionOlder stateVersion "21.03" then nextcloud20
-          else if versionOlder stateVersion "21.11" then nextcloud21
           else if versionOlder stateVersion "22.05" then nextcloud22
-          else nextcloud23
+          else nextcloud24
         );
 
-      services.nextcloud.datadir = mkOptionDefault config.services.nextcloud.home;
-
       services.nextcloud.phpPackage =
-        if versionOlder cfg.package.version "21" then pkgs.php74
+        if versionOlder cfg.package.version "24" then pkgs.php80
+        # FIXME: Use PHP 8.1 with Nextcloud 24 and higher, once issues like this one are fixed:
+        #
+        # https://github.com/nextcloud/twofactor_totp/issues/1192
+        #
+        # else if versionOlder cfg.package.version "24" then pkgs.php80
+        # else pkgs.php81;
         else pkgs.php80;
     }
 
+    { assertions = [
+      { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql";
+        message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.'';
+      }
+    ]; }
+
     { systemd.timers.nextcloud-cron = {
         wantedBy = [ "timers.target" ];
+        after = [ "nextcloud-setup.service" ];
         timerConfig.OnBootSec = "5m";
         timerConfig.OnUnitActiveSec = "5m";
         timerConfig.Unit = "nextcloud-cron.service";
@@ -636,6 +721,8 @@ in {
               if x == null then "false"
               else boolToString x;
 
+          nextcloudGreaterOrEqualThan = req: versionAtLeast cfg.package.version req;
+
           overrideConfig = pkgs.writeText "nextcloud-config.php" ''
             <?php
             ${optionalString requiresReadSecretFunction ''
@@ -648,10 +735,20 @@ in {
                     $file
                   ));
                 }
-
                 return trim(file_get_contents($file));
+              }''}
+            function nix_decode_json_file($file, $error) {
+              if (!file_exists($file)) {
+                throw new \RuntimeException(sprintf($error, $file));
               }
-            ''}
+              $decoded = json_decode(file_get_contents($file), true);
+
+              if (json_last_error() !== JSON_ERROR_NONE) {
+                throw new \RuntimeException(sprintf("Cannot decode %s, because: %s", $file, json_last_error_msg()));
+              }
+
+              return $decoded;
+            }
             $CONFIG = [
               'apps_paths' => [
                 ${optionalString (cfg.extraApps != { }) "[ 'path' => '${cfg.home}/nix-apps', 'url' => '/nix-apps', 'writable' => false ],"}
@@ -663,20 +760,38 @@ in {
               'skeletondirectory' => '${cfg.skeletonDirectory}',
               ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
               'log_type' => 'syslog',
-              'log_level' => '${builtins.toString cfg.logLevel}',
+              'loglevel' => '${builtins.toString cfg.logLevel}',
               ${optionalString (c.overwriteProtocol != null) "'overwriteprotocol' => '${c.overwriteProtocol}',"}
               ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
               ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
               ${optionalString (c.dbport != null) "'dbport' => '${toString c.dbport}',"}
               ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
               ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
-              ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('${c.dbpassFile}'),"}
+              ${optionalString (c.dbpassFile != null) ''
+                  'dbpassword' => nix_read_secret(
+                    "${c.dbpassFile}"
+                  ),
+                ''
+              }
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
               'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
               ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
+              ${optionalString (nextcloudGreaterOrEqualThan "23") "'profile.enabled' => ${boolToString cfg.globalProfiles},"}
               ${objectstoreConfig}
             ];
+
+            $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
+              "${jsonFormat.generate "nextcloud-extraOptions.json" cfg.extraOptions}",
+              "impossible: this should never happen (decoding generated extraOptions file %s failed)"
+            ));
+
+            ${optionalString (cfg.secretFile != null) ''
+              $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
+                "${cfg.secretFile}",
+                "Cannot start Nextcloud, secrets file %s set by NixOS doesn't exist!"
+              ));
+            ''}
           '';
           occInstallCmd = let
             mkExport = { arg, value }: "export ${arg}=${value}";
@@ -771,7 +886,7 @@ in {
             ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
 
             ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
-                # Try to enable apps (don't fail when one of them cannot be enabled , eg. due to incompatible version)
+                # Try to enable apps
                 ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
             ''}
 
@@ -781,12 +896,14 @@ in {
           serviceConfig.User = "nextcloud";
         };
         nextcloud-cron = {
+          after = [ "nextcloud-setup.service" ];
           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
           serviceConfig.Type = "oneshot";
           serviceConfig.User = "nextcloud";
           serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${cfg.package}/cron.php";
         };
         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
+          after = [ "nextcloud-setup.service" ];
           serviceConfig.Type = "oneshot";
           serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
           serviceConfig.User = "nextcloud";
@@ -820,6 +937,32 @@ in {
 
       environment.systemPackages = [ occ ];
 
+      services.mysql = lib.mkIf cfg.database.createLocally {
+        enable = true;
+        package = lib.mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.config.dbname ];
+        ensureUsers = [{
+          name = cfg.config.dbuser;
+          ensurePermissions = { "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; };
+        }];
+        # FIXME(@Ma27) Nextcloud isn't compatible with mariadb 10.6,
+        # this is a workaround.
+        # See https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/22
+        settings = mkIf (versionOlder cfg.package.version "24") {
+          mysqld = {
+            innodb_read_only_compressed = 0;
+          };
+        };
+        initialScript = pkgs.writeText "mysql-init" ''
+          CREATE USER '${cfg.config.dbname}'@'localhost' IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
+          CREATE DATABASE IF NOT EXISTS ${cfg.config.dbname};
+          GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
+            CREATE TEMPORARY TABLES ON ${cfg.config.dbname}.* TO '${cfg.config.dbuser}'@'localhost'
+            IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
+          FLUSH privileges;
+        '';
+      };
+
       services.nginx.enable = mkDefault true;
 
       services.nginx.virtualHosts.${cfg.hostName} = {
@@ -829,7 +972,6 @@ in {
             priority = 100;
             extraConfig = ''
               allow all;
-              log_not_found off;
               access_log off;
             '';
           };
@@ -917,7 +1059,9 @@ in {
             add_header X-Permitted-Cross-Domain-Policies none;
             add_header X-Frame-Options sameorigin;
             add_header Referrer-Policy no-referrer;
-            add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
+          ''}
+          ${optionalString (cfg.https) ''
+            add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always;
           ''}
           client_max_body_size ${cfg.maxUploadSize};
           fastcgi_buffers 64 4K;
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index 8f55086a2bd1f..b46f34420a703 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -11,7 +11,7 @@
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
  <para>
-  The current default by NixOS is <package>nextcloud23</package> which is also the latest
+  The current default by NixOS is <package>nextcloud24</package> which is also the latest
   major version available.
  </para>
  <section xml:id="module-services-nextcloud-basic-usage">
diff --git a/nixos/modules/services/web-apps/nifi.nix b/nixos/modules/services/web-apps/nifi.nix
new file mode 100644
index 0000000000000..21a631272641a
--- /dev/null
+++ b/nixos/modules/services/web-apps/nifi.nix
@@ -0,0 +1,318 @@
+{ lib, pkgs, config, options, ... }:
+
+let
+  cfg = config.services.nifi;
+  opt = options.services.nifi;
+
+  env = {
+    NIFI_OVERRIDE_NIFIENV = "true";
+    NIFI_HOME = "/var/lib/nifi";
+    NIFI_PID_DIR = "/run/nifi";
+    NIFI_LOG_DIR = "/var/log/nifi";
+  };
+
+  envFile = pkgs.writeText "nifi.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  nifiEnv = pkgs.writeShellScriptBin "nifi-env" ''
+    set -a
+    source "${envFile}"
+    eval -- "\$@"
+  '';
+
+in {
+  options = {
+    services.nifi = {
+      enable = lib.mkEnableOption "Apache NiFi";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.nifi;
+        defaultText = lib.literalExpression "pkgs.nifi";
+        description = "Apache NiFi package to use.";
+      };
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        default = "nifi";
+        description = "User account where Apache NiFi runs.";
+      };
+
+      group = lib.mkOption {
+        type = lib.types.str;
+        default = "nifi";
+        description = "Group account where Apache NiFi runs.";
+      };
+
+      enableHTTPS = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = "Enable HTTPS protocol. Don`t use in production.";
+      };
+
+      listenHost = lib.mkOption {
+        type = lib.types.str;
+        default = if cfg.enableHTTPS then "0.0.0.0" else "127.0.0.1";
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "0.0.0.0"
+          else "127.0.0.1"
+        '';
+        description = "Bind to an ip for Apache NiFi web-ui.";
+      };
+
+      listenPort = lib.mkOption {
+        type = lib.types.int;
+        default = if cfg.enableHTTPS then 8443 else 8080;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "8443"
+          else "8000"
+        '';
+        description = "Bind to a port for Apache NiFi web-ui.";
+      };
+
+      proxyHost = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = if cfg.enableHTTPS then "0.0.0.0" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "0.0.0.0"
+          else null
+        '';
+        description = "Allow requests from a specific host.";
+      };
+
+      proxyPort = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = if cfg.enableHTTPS then 8443 else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.enableHTTPS}
+          then "8443"
+          else null
+        '';
+        description = "Allow requests from a specific port.";
+      };
+
+      initUser = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = "Initial user account for Apache NiFi. Username must be at least 4 characters.";
+      };
+
+      initPasswordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/nifi/password-nifi";
+        description = "nitial password for Apache NiFi. Password must be at least 12 characters.";
+      };
+
+      initJavaHeapSize = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = null;
+        example = 1024;
+        description = "Set the initial heap size for the JVM in MB.";
+      };
+
+      maxJavaHeapSize = lib.mkOption {
+        type = lib.types.nullOr lib.types.int;
+        default = null;
+        example = 2048;
+        description = "Set the initial heap size for the JVM in MB.";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.initUser!=null || cfg.initPasswordFile==null;
+          message = ''
+            <option>services.nifi.initUser</option> needs to be set if <option>services.nifi.initPasswordFile</option> enabled.
+          '';
+      }
+      { assertion = cfg.initUser==null || cfg.initPasswordFile!=null;
+          message = ''
+            <option>services.nifi.initPasswordFile</option> needs to be set if <option>services.nifi.initUser</option> enabled.
+          '';
+      }
+      { assertion = cfg.proxyHost==null || cfg.proxyPort!=null;
+          message = ''
+            <option>services.nifi.proxyPort</option> needs to be set if <option>services.nifi.proxyHost</option> value specified.
+          '';
+      }
+      { assertion = cfg.proxyHost!=null || cfg.proxyPort==null;
+          message = ''
+            <option>services.nifi.proxyHost</option> needs to be set if <option>services.nifi.proxyPort</option> value specified.
+          '';
+      }
+      { assertion = cfg.initJavaHeapSize==null || cfg.maxJavaHeapSize!=null;
+          message = ''
+            <option>services.nifi.maxJavaHeapSize</option> needs to be set if <option>services.nifi.initJavaHeapSize</option> value specified.
+          '';
+      }
+      { assertion = cfg.initJavaHeapSize!=null || cfg.maxJavaHeapSize==null;
+          message = ''
+            <option>services.nifi.initJavaHeapSize</option> needs to be set if <option>services.nifi.maxJavaHeapSize</option> value specified.
+          '';
+      }
+    ];
+
+    warnings = lib.optional (cfg.enableHTTPS==false) ''
+      Please do not disable HTTPS mode in production. In this mode, access to the nifi is opened without authentication.
+    '';
+
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/nifi/conf' 0750 ${cfg.user} ${cfg.group}"
+      "L+ '/var/lib/nifi/lib' - - - - ${cfg.package}/lib"
+    ];
+
+
+    systemd.services.nifi = {
+      description = "Apache NiFi";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = env;
+      path = [ pkgs.gawk ];
+
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/nifi/nifi.pid";
+        ExecStartPre = pkgs.writeScript "nifi-pre-start.sh" ''
+          #!/bin/sh
+          umask 077
+          test -f '/var/lib/nifi/conf/authorizers.xml'                      || (cp '${cfg.package}/share/nifi/conf/authorizers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/authorizers.xml')
+          test -f '/var/lib/nifi/conf/bootstrap.conf'                       || (cp '${cfg.package}/share/nifi/conf/bootstrap.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap.conf')
+          test -f '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf'       || (cp '${cfg.package}/share/nifi/conf/bootstrap-hashicorp-vault.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf')
+          test -f '/var/lib/nifi/conf/bootstrap-notification-services.xml'  || (cp '${cfg.package}/share/nifi/conf/bootstrap-notification-services.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-notification-services.xml')
+          test -f '/var/lib/nifi/conf/logback.xml'                          || (cp '${cfg.package}/share/nifi/conf/logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/logback.xml')
+          test -f '/var/lib/nifi/conf/login-identity-providers.xml'         || (cp '${cfg.package}/share/nifi/conf/login-identity-providers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/login-identity-providers.xml')
+          test -f '/var/lib/nifi/conf/nifi.properties'                      || (cp '${cfg.package}/share/nifi/conf/nifi.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/nifi.properties')
+          test -f '/var/lib/nifi/conf/stateless-logback.xml'                || (cp '${cfg.package}/share/nifi/conf/stateless-logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless-logback.xml')
+          test -f '/var/lib/nifi/conf/stateless.properties'                 || (cp '${cfg.package}/share/nifi/conf/stateless.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless.properties')
+          test -f '/var/lib/nifi/conf/state-management.xml'                 || (cp '${cfg.package}/share/nifi/conf/state-management.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/state-management.xml')
+          test -f '/var/lib/nifi/conf/zookeeper.properties'                 || (cp '${cfg.package}/share/nifi/conf/zookeeper.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/zookeeper.properties')
+          test -d '/var/lib/nifi/docs/html'                                 || (mkdir -p /var/lib/nifi/docs && cp -r '${cfg.package}/share/nifi/docs/html' '/var/lib/nifi/docs/html')
+          ${lib.optionalString ((cfg.initUser != null) && (cfg.initPasswordFile != null)) ''
+            awk -F'[<|>]' '/property name="Username"/ {if ($3!="") f=1} END{exit !f}' /var/lib/nifi/conf/login-identity-providers.xml || ${cfg.package}/bin/nifi.sh set-single-user-credentials ${cfg.initUser} $(cat ${cfg.initPasswordFile})
+          ''}
+          ${lib.optionalString (cfg.enableHTTPS == false) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=false|g' \
+              -e 's|nifi.web.http.host=.*|nifi.web.http.host=${cfg.listenHost}|g' \
+              -e 's|nifi.web.http.port=.*|nifi.web.http.port=${(toString cfg.listenPort)}|g' \
+              -e 's|nifi.web.https.host=.*|nifi.web.https.host=|g' \
+              -e 's|nifi.web.https.port=.*|nifi.web.https.port=|g' \
+              -e 's|nifi.security.keystore=.*|nifi.security.keystore=|g' \
+              -e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=|g' \
+              -e 's|nifi.security.truststore=.*|nifi.security.truststore=|g' \
+              -e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=|g' \
+              -e '/nifi.security.keystorePasswd/s|^|#|' \
+              -e '/nifi.security.keyPasswd/s|^|#|' \
+              -e '/nifi.security.truststorePasswd/s|^|#|'
+          ''}
+          ${lib.optionalString (cfg.enableHTTPS == true) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=true|g' \
+              -e 's|nifi.web.http.host=.*|nifi.web.http.host=|g' \
+              -e 's|nifi.web.http.port=.*|nifi.web.http.port=|g' \
+              -e 's|nifi.web.https.host=.*|nifi.web.https.host=${cfg.listenHost}|g' \
+              -e 's|nifi.web.https.port=.*|nifi.web.https.port=${(toString cfg.listenPort)}|g' \
+              -e 's|nifi.security.keystore=.*|nifi.security.keystore=./conf/keystore.p12|g' \
+              -e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=PKCS12|g' \
+              -e 's|nifi.security.truststore=.*|nifi.security.truststore=./conf/truststore.p12|g' \
+              -e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=PKCS12|g' \
+              -e '/nifi.security.keystorePasswd/s|^#\+||' \
+              -e '/nifi.security.keyPasswd/s|^#\+||' \
+              -e '/nifi.security.truststorePasswd/s|^#\+||'
+          ''}
+          ${lib.optionalString ((cfg.enableHTTPS == true) && (cfg.proxyHost != null) && (cfg.proxyPort != null)) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=${cfg.proxyHost}:${(toString cfg.proxyPort)}|g'
+          ''}
+          ${lib.optionalString ((cfg.enableHTTPS == false) || (cfg.proxyHost == null) && (cfg.proxyPort == null)) ''
+            sed -i /var/lib/nifi/conf/nifi.properties \
+              -e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=|g'
+          ''}
+          ${lib.optionalString ((cfg.initJavaHeapSize != null) && (cfg.maxJavaHeapSize != null))''
+            sed -i /var/lib/nifi/conf/bootstrap.conf \
+              -e 's|java.arg.2=.*|java.arg.2=-Xms${(toString cfg.initJavaHeapSize)}m|g' \
+              -e 's|java.arg.3=.*|java.arg.3=-Xmx${(toString cfg.maxJavaHeapSize)}m|g'
+          ''}
+          ${lib.optionalString ((cfg.initJavaHeapSize == null) && (cfg.maxJavaHeapSize == null))''
+            sed -i /var/lib/nifi/conf/bootstrap.conf \
+              -e 's|java.arg.2=.*|java.arg.2=-Xms512m|g' \
+              -e 's|java.arg.3=.*|java.arg.3=-Xmx512m|g'
+          ''}
+        '';
+        ExecStart = "${cfg.package}/bin/nifi.sh start";
+        ExecStop = "${cfg.package}/bin/nifi.sh stop";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Runtime directory and mode
+        RuntimeDirectory = "nifi";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "nifi";
+        StateDirectoryMode = "0750";
+        # Logs directory and mode
+        LogsDirectory = "nifi";
+        LogsDirectoryMode = "0750";
+        # Proc filesystem
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
+        # Access write directories
+        ReadWritePaths = [ cfg.initPasswordFile ];
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute  = false;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @resources @privileged @setuid" "@chown" ];
+      };
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "nifi") {
+        nifi = {
+          group = cfg.group;
+          isSystemUser = true;
+          home = cfg.package;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package nifiEnv ])
+    ];
+
+    users.groups = lib.optionalAttrs (cfg.group == "nifi") {
+      nifi = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/onlyoffice.nix b/nixos/modules/services/web-apps/onlyoffice.nix
new file mode 100644
index 0000000000000..ce6a3e835e3b3
--- /dev/null
+++ b/nixos/modules/services/web-apps/onlyoffice.nix
@@ -0,0 +1,288 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.onlyoffice;
+in
+{
+  options.services.onlyoffice = {
+    enable = mkEnableOption "OnlyOffice DocumentServer";
+
+    enableExampleServer = mkEnableOption "OnlyOffice example server";
+
+    hostname = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "FQDN for the onlyoffice instance.";
+    };
+
+    jwtSecretFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Path to a file that contains the secret to sign web requests using JSON Web Tokens.
+        If left at the default value null signing is disabled.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.onlyoffice-documentserver;
+      defaultText = "pkgs.onlyoffice-documentserver";
+      description = "Which package to use for the OnlyOffice instance.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = "Port the OnlyOffice DocumentServer should listens on.";
+    };
+
+    examplePort = mkOption {
+      type = types.port;
+      default = null;
+      description = "Port the OnlyOffice Example server should listens on.";
+    };
+
+    postgresHost = mkOption {
+      type = types.str;
+      default = "/run/postgresql";
+      description = "The Postgresql hostname or socket path OnlyOffice should connect to.";
+    };
+
+    postgresName = mkOption {
+      type = types.str;
+      default = "onlyoffice";
+      description = "The name of databse OnlyOffice should user.";
+    };
+
+    postgresPasswordFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Path to a file that contains the password OnlyOffice should use to connect to Postgresql.
+        Unused when using socket authentication.
+      '';
+    };
+
+    postgresUser = mkOption {
+      type = types.str;
+      default = "onlyoffice";
+      description = ''
+        The username OnlyOffice should use to connect to Postgresql.
+        Unused when using socket authentication.
+      '';
+    };
+
+    rabbitmqUrl = mkOption {
+      type = types.str;
+      default = "amqp://guest:guest@localhost:5672";
+      description = "The Rabbitmq in amqp URI style OnlyOffice should connect to.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services = {
+      nginx = {
+        enable = mkDefault true;
+        # misses text/csv, font/ttf, application/x-font-ttf, application/rtf, application/wasm
+        recommendedGzipSettings = mkDefault true;
+        recommendedProxySettings = mkDefault true;
+
+        upstreams = {
+          # /etc/nginx/includes/http-common.conf
+          onlyoffice-docservice = {
+            servers = { "localhost:${toString cfg.port}" = { }; };
+          };
+          onlyoffice-example = lib.mkIf cfg.enableExampleServer {
+            servers = { "localhost:${toString cfg.examplePort}" = { }; };
+          };
+        };
+
+        virtualHosts.${cfg.hostname} = {
+          locations = {
+            # /etc/nginx/includes/ds-docservice.conf
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps\/apps\/api\/documents\/api\.js)$".extraConfig = ''
+              expires -1;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps)(\/.*\.json)$".extraConfig = ''
+              expires 365d;
+              error_log /dev/null crit;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(sdkjs-plugins)(\/.*\.json)$".extraConfig = ''
+              expires 365d;
+              error_log /dev/null crit;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(web-apps|sdkjs|sdkjs-plugins|fonts)(\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            "~* ^(\/cache\/files.*)(\/.*)".extraConfig = ''
+              alias /var/lib/onlyoffice/documentserver/App_Data$1;
+              add_header Content-Disposition "attachment; filename*=UTF-8''$arg_filename";
+
+              set $secret_string verysecretstring;
+              secure_link $arg_md5,$arg_expires;
+              secure_link_md5 "$secure_link_expires$uri$secret_string";
+
+              if ($secure_link = "") {
+                return 403;
+              }
+
+              if ($secure_link = "0") {
+                return 410;
+              }
+            '';
+            "~* ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(internal)(\/.*)$".extraConfig = ''
+              allow 127.0.0.1;
+              deny all;
+              proxy_pass http://onlyoffice-docservice/$2$3;
+            '';
+            "~* ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(info)(\/.*)$".extraConfig = ''
+              allow 127.0.0.1;
+              deny all;
+              proxy_pass http://onlyoffice-docservice/$2$3;
+            '';
+            "/".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?(\/doc\/.*)".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice$2;
+              proxy_http_version 1.1;
+            '';
+            "/${cfg.package.version}/".extraConfig = ''
+              proxy_pass http://onlyoffice-docservice/;
+            '';
+            "~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/(dictionaries)(\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
+            '';
+            # /etc/nginx/includes/ds-example.conf
+            "~ ^(\/welcome\/.*)$".extraConfig = ''
+              expires 365d;
+              alias ${cfg.package}/var/www/onlyoffice/documentserver-example$1;
+              index docker.html;
+            '';
+            "/example/".extraConfig = lib.mkIf cfg.enableExampleServer ''
+              proxy_pass http://onlyoffice-example/;
+              proxy_set_header X-Forwarded-Path /example;
+            '';
+          };
+          extraConfig = ''
+            rewrite ^/$ /welcome/ redirect;
+            rewrite ^\/OfficeWeb(\/apps\/.*)$ /${cfg.package.version}/web-apps$1 redirect;
+            rewrite ^(\/web-apps\/apps\/(?!api\/).*)$ /${cfg.package.version}$1 redirect;
+
+            # based on https://github.com/ONLYOFFICE/document-server-package/blob/master/common/documentserver/nginx/includes/http-common.conf.m4#L29-L34
+            # without variable indirection and correct variable names
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-Host $host;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            # required for CSP to take effect
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            # required for websocket
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection $connection_upgrade;
+          '';
+        };
+      };
+
+      rabbitmq.enable = lib.mkDefault true;
+
+      postgresql = {
+        enable = lib.mkDefault true;
+        ensureDatabases = [ "onlyoffice" ];
+        ensureUsers = [{
+          name = "onlyoffice";
+          ensurePermissions = { "DATABASE \"onlyoffice\"" = "ALL PRIVILEGES"; };
+        }];
+      };
+    };
+
+    systemd.services = {
+      onlyoffice-converter = {
+        description = "onlyoffice converter";
+        after = [ "network.target" "onlyoffice-docservice.service" "postgresql.service" ];
+        requires = [ "network.target" "onlyoffice-docservice.service" "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper FileConverter/converter /run/onlyoffice/config";
+          Group = "onlyoffice";
+          Restart = "always";
+          RuntimeDirectory = "onlyoffice";
+          StateDirectory = "onlyoffice";
+          Type = "simple";
+          User = "onlyoffice";
+        };
+      };
+
+      onlyoffice-docservice =
+        let
+          onlyoffice-prestart = pkgs.writeShellScript "onlyoffice-prestart" ''
+            PATH=$PATH:${lib.makeBinPath (with pkgs; [ jq moreutils config.services.postgresql.package ])}
+            umask 077
+            mkdir -p /run/onlyoffice/config/ /var/lib/onlyoffice/documentserver/sdkjs/{slide/themes,common}/ /var/lib/onlyoffice/documentserver/{fonts,server/FileConverter/bin}/
+            cp -r ${cfg.package}/etc/onlyoffice/documentserver/* /run/onlyoffice/config/
+            chmod u+w /run/onlyoffice/config/default.json
+
+            cp /run/onlyoffice/config/default.json{,.orig}
+
+            # for a mapping of environment variables from the docker container to json options see
+            # https://github.com/ONLYOFFICE/Docker-DocumentServer/blob/master/run-document-server.sh
+            jq '
+              .services.CoAuthoring.server.port = ${toString cfg.port} |
+              .services.CoAuthoring.sql.dbHost = "${cfg.postgresHost}" |
+              .services.CoAuthoring.sql.dbName = "${cfg.postgresName}" |
+            ${lib.optionalString (cfg.postgresPasswordFile != null) ''
+              .services.CoAuthoring.sql.dbPass = "'"$(cat ${cfg.postgresPasswordFile})"'" |
+            ''}
+              .services.CoAuthoring.sql.dbUser = "${cfg.postgresUser}" |
+            ${lib.optionalString (cfg.jwtSecretFile != null) ''
+              .services.CoAuthoring.token.enable.browser = true |
+              .services.CoAuthoring.token.enable.request.inbox = true |
+              .services.CoAuthoring.token.enable.request.outbox = true |
+              .services.CoAuthoring.secret.inbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+              .services.CoAuthoring.secret.outbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+              .services.CoAuthoring.secret.session.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
+            ''}
+              .rabbitmq.url = "${cfg.rabbitmqUrl}"
+              ' /run/onlyoffice/config/default.json | sponge /run/onlyoffice/config/default.json
+
+            if ! psql -d onlyoffice -c "SELECT 'task_result'::regclass;" >/dev/null; then
+              psql -f ${cfg.package}/var/www/onlyoffice/documentserver/server/schema/postgresql/createdb.sql
+            fi
+          '';
+        in
+        {
+          description = "onlyoffice documentserver";
+          after = [ "network.target" "postgresql.service" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper DocService/docservice /run/onlyoffice/config";
+            ExecStartPre = onlyoffice-prestart;
+            Group = "onlyoffice";
+            Restart = "always";
+            RuntimeDirectory = "onlyoffice";
+            StateDirectory = "onlyoffice";
+            Type = "simple";
+            User = "onlyoffice";
+          };
+        };
+    };
+
+    users.users = {
+      onlyoffice = {
+        description = "OnlyOffice Service";
+        group = "onlyoffice";
+        isSystemUser = true;
+      };
+    };
+
+    users.groups.onlyoffice = { };
+  };
+}
diff --git a/nixos/modules/services/web-apps/openwebrx.nix b/nixos/modules/services/web-apps/openwebrx.nix
index 9e90c01e0bbb0..8a7972a3fc6ec 100644
--- a/nixos/modules/services/web-apps/openwebrx.nix
+++ b/nixos/modules/services/web-apps/openwebrx.nix
@@ -19,6 +19,10 @@ in
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [
         csdr
+        digiham
+        codec2
+        js8call
+        m17-cxx-demod
         alsaUtils
         netcat
       ];
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index a65428018260d..e6b6aa273e7fb 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -209,7 +209,7 @@ in {
 
       port = lib.mkOption {
         type = lib.types.nullOr lib.types.port;
-        default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 6379;
+        default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 31638;
         defaultText = lib.literalExpression ''
           if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
           then null
@@ -320,6 +320,7 @@ in {
         };
         storage = {
           tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
+          bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
           avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
           videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
           streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
@@ -333,8 +334,17 @@ in {
           plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
           client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
         };
+        import = {
+          videos = {
+            http = {
+              youtube_dl_release = {
+                python_path = "${pkgs.python3}/bin/python";
+              };
+            };
+          };
+        };
       }
-      (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis/redis.sock"; }; })
+      (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis-peertube/redis.sock"; }; })
     ];
 
     systemd.tmpfiles.rules = [
@@ -380,7 +390,7 @@ in {
 
       environment = env;
 
-      path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn youtube-dl ];
+      path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn python3 ];
 
       script = ''
         #!/bin/sh
@@ -431,13 +441,17 @@ in {
       enable = true;
     };
 
-    services.redis = lib.mkMerge [
+    services.redis.servers.peertube = lib.mkMerge [
       (lib.mkIf cfg.redis.createLocally {
         enable = true;
       })
+      (lib.mkIf (cfg.redis.createLocally && !cfg.redis.enableUnixSocket) {
+        bind = "127.0.0.1";
+        port = cfg.redis.port;
+      })
       (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
-        unixSocket = "/run/redis/redis.sock";
-        unixSocketPerm = 770;
+        unixSocket = "/run/redis-peertube/redis.sock";
+        unixSocketPerm = 660;
       })
     ];
 
@@ -455,7 +469,7 @@ in {
         };
       })
       (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
-      (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis" ];})
+      (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
     users.groups = lib.optionalAttrs (cfg.group == "peertube") {
diff --git a/nixos/modules/services/web-apps/phylactery.nix b/nixos/modules/services/web-apps/phylactery.nix
new file mode 100644
index 0000000000000..f0e97da1f2023
--- /dev/null
+++ b/nixos/modules/services/web-apps/phylactery.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.phylactery;
+in {
+  options.services.phylactery = {
+    enable = mkEnableOption "Whether to enable Phylactery server";
+
+    host = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Listen host for Phylactery";
+    };
+
+    port = mkOption {
+      type = types.port;
+      description = "Listen port for Phylactery";
+    };
+
+    library = mkOption {
+      type = types.path;
+      description = "Path to CBZ library";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.phylactery;
+      defaultText = literalExpression "pkgs.phylactery";
+      description = "The Phylactery package to use";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.phylactery = {
+      environment = {
+        PHYLACTERY_ADDRESS = "${cfg.host}:${toString cfg.port}";
+        PHYLACTERY_LIBRARY = "${cfg.library}";
+      };
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ConditionPathExists = cfg.library;
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/phylactery";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ McSinyx ];
+}
diff --git a/nixos/modules/services/web-apps/plantuml-server.nix b/nixos/modules/services/web-apps/plantuml-server.nix
index f4bf43f56b98a..9ea37b8a4cad4 100644
--- a/nixos/modules/services/web-apps/plantuml-server.nix
+++ b/nixos/modules/services/web-apps/plantuml-server.nix
@@ -20,6 +20,21 @@ in
         description = "PlantUML server package to use";
       };
 
+      packages = {
+        jdk = mkOption {
+          type = types.package;
+          default = pkgs.jdk;
+          defaultText = literalExpression "pkgs.jdk";
+          description = "JDK package to use for the server";
+        };
+        jetty = mkOption {
+          type = types.package;
+          default = pkgs.jetty;
+          defaultText = literalExpression "pkgs.jetty";
+          description = "Jetty package to use for the server";
+        };
+      };
+
       user = mkOption {
         type = types.str;
         default = "plantuml";
@@ -105,10 +120,10 @@ in
         ALLOW_PLANTUML_INCLUDE = if cfg.allowPlantumlInclude then "true" else "false";
       };
       script = ''
-      ${pkgs.jre}/bin/java \
-        -jar ${pkgs.jetty}/start.jar \
+      ${cfg.packages.jdk}/bin/java \
+        -jar ${cfg.packages.jetty}/start.jar \
           --module=deploy,http,jsp \
-          jetty.home=${pkgs.jetty} \
+          jetty.home=${cfg.packages.jetty} \
           jetty.base=${cfg.package} \
           jetty.http.host=${cfg.listenHost} \
           jetty.http.port=${builtins.toString cfg.listenPort}
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
index fd97ab76a5f62..1a8199ab3b37b 100644
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -235,7 +235,7 @@ in
       locations."~ \\.php$" = {
         tryFiles = "$uri =404";
         extraConfig = ''
-          include ${pkgs.nginx}/conf/fastcgi_params;
+          include ${config.services.nginx.package}/conf/fastcgi_params;
           fastcgi_pass    unix:${fpm.socket};
           fastcgi_index   index.php;
           fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
@@ -263,8 +263,8 @@ in
       serviceConfig.RemainAfterExit = true;
 
       wantedBy = [ "multi-user.target" ];
-      requires = [ "postgresql.service" ];
-      after = [ "network.target" "postgresql.service" ];
+      requires = if cfg.database.host == null then [] else [ "postgresql.service" ];
+      after = [ "network.target" ] ++ (if cfg.database.host == null then [] else [ "postgresql.service" ]);
 
       script = ''
         rm -rf "${runDir}"
@@ -282,7 +282,7 @@ in
           sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
         '' else ''
           sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
-          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'file_get_contents(${cfg.database.passwordFile})'"});/g" "${runDir}/server/php/config.inc.php
+          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'$(cat ${cfg.database.passwordFile})');/g"}" "${runDir}/server/php/config.inc.php"
         ''}
         sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
         sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
@@ -294,7 +294,7 @@ in
         ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
 
         chmod g+w "${runDir}/tmp/cache"
-        chown -R "${cfg.user}"."${cfg.group}" "${runDir}"
+        chown -R "${cfg.user}":"${cfg.group}" "${runDir}"
 
 
         mkdir -m 0750 -p "${cfg.dataDir}"
@@ -302,9 +302,9 @@ in
         mkdir -m 0750 -p "${cfg.dataDir}/client/img"
         cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media"
         cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img"
-        chown "${cfg.user}"."${cfg.group}" "${cfg.dataDir}"
-        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/media"
-        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/client/img"
+        chown "${cfg.user}":"${cfg.group}" "${cfg.dataDir}"
+        chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/media"
+        chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/client/img"
 
         ${optionalString (cfg.database.host == null) ''
           if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then
diff --git a/nixos/modules/services/web-apps/rss-bridge.nix b/nixos/modules/services/web-apps/rss-bridge.nix
index 456ca00416feb..f2b6d9559823b 100644
--- a/nixos/modules/services/web-apps/rss-bridge.nix
+++ b/nixos/modules/services/web-apps/rss-bridge.nix
@@ -111,7 +111,7 @@ in
 
           locations."~ ^/index.php(/|$)" = {
             extraConfig = ''
-              include ${pkgs.nginx}/conf/fastcgi_params;
+              include ${config.services.nginx.package}/conf/fastcgi_params;
               fastcgi_split_path_info ^(.+\.php)(/.+)$;
               fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
diff --git a/nixos/modules/services/web-apps/snipe-it.nix b/nixos/modules/services/web-apps/snipe-it.nix
new file mode 100644
index 0000000000000..842e0715c0256
--- /dev/null
+++ b/nixos/modules/services/web-apps/snipe-it.nix
@@ -0,0 +1,493 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snipe-it;
+  snipe-it = pkgs.snipe-it.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "snipe-it" ''
+    #! ${pkgs.runtimeShell}
+    cd ${snipe-it}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${pkgs.php}/bin/php artisan $*
+  '';
+in {
+  options.services.snipe-it = {
+
+    enable = mkEnableOption "A free open source IT asset/license management system";
+
+    user = mkOption {
+      default = "snipeit";
+      description = "User snipe-it runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "snipeit";
+      description = "Group snipe-it runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = ''
+        A file containing the Laravel APP_KEY - a 32 character long,
+        base64 encoded key used for encryption where needed. Can be
+        generated with <code>head -c 32 /dev/urandom | base64</code>.
+      '';
+      example = "/run/keys/snipe-it/appkey";
+      type = types.path;
+    };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = if config.networking.domain != null then
+                  config.networking.fqdn
+                else
+                  config.networking.hostName;
+      defaultText = lib.literalExpression "config.networking.fqdn";
+      example = "snipe-it.example.com";
+      description = ''
+        The hostname to serve Snipe-IT on.
+      '';
+    };
+
+    appURL = mkOption {
+      description = ''
+        The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
+        If you change this in the future you may need to run a command to update stored URLs in the database.
+        Command example: <code>snipe-it snipe-it:update-url https://old.example.com https://new.example.com</code>
+      '';
+      default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
+      defaultText = ''
+        http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
+      '';
+      example = "https://example.com";
+      type = types.str;
+    };
+
+    dataDir = mkOption {
+      description = "snipe-it data directory";
+      default = "/var/lib/snipe-it";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "snipeit";
+        description = "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = literalExpression "user";
+        description = "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/snipe-it/dbpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum [ "smtp" "sendmail" ];
+        default = "smtp";
+        description = "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "Mail host port.";
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum [ "tls" "ssl" ]);
+        default = null;
+        description = "SMTP encryption mechanism to use.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "snipeit";
+        description = "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/snipe-it/mailpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>mail.user</option>.
+        '';
+      };
+      backupNotificationAddress = mkOption {
+        type = types.str;
+        default = "backup@example.com";
+        description = "Email Address to send Backup Notifications to.";
+      };
+      from = {
+        name = mkOption {
+          type = types.str;
+          default = "Snipe-IT Asset Management";
+          description = "Mail \"from\" name.";
+        };
+        address = mkOption {
+          type = types.str;
+          default = "mail@example.com";
+          description = "Mail \"from\" address.";
+        };
+      };
+      replyTo = {
+        name = mkOption {
+          type = types.str;
+          default = "Snipe-IT Asset Management";
+          description = "Mail \"reply-to\" name.";
+        };
+        address = mkOption {
+          type = types.str;
+          default = "mail@example.com";
+          description = "Mail \"reply-to\" address.";
+        };
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = "The maximum size for uploads (e.g. images).";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for the snipe-it PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    nginx = mkOption {
+      type = types.submodule (
+        recursiveUpdate
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
+      );
+      default = {};
+      example = literalExpression ''
+        {
+          serverAliases = [
+            "snipe-it.''${config.networking.domain}"
+          ];
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = ''
+        With this option, you can customize the nginx virtualHost settings.
+      '';
+    };
+
+    config = mkOption {
+      type = with types;
+        attrsOf
+          (nullOr
+            (either
+              (oneOf [
+                bool
+                int
+                port
+                path
+                str
+              ])
+              (submodule {
+                options = {
+                  _secret = mkOption {
+                    type = nullOr (oneOf [ str path ]);
+                    description = ''
+                      The path to a file containing the value the
+                      option should be set to in the final
+                      configuration file.
+                    '';
+                  };
+                };
+              })));
+      default = {};
+      example = literalExpression ''
+        {
+          ALLOWED_IFRAME_HOSTS = "https://example.com";
+          WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
+          AUTH_METHOD = "oidc";
+          OIDC_NAME = "MyLogin";
+          OIDC_DISPLAY_NAME_CLAIMS = "name";
+          OIDC_CLIENT_ID = "snipe-it";
+          OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
+          OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
+          OIDC_ISSUER_DISCOVER = true;
+        }
+      '';
+      description = ''
+        Snipe-IT configuration options to set in the
+        <filename>.env</filename> file.
+        Refer to <link xlink:href="https://snipe-it.readme.io/docs/configuration"/>
+        for details on supported values.
+
+        Settings containing secret data should be set to an attribute
+        set containing the attribute <literal>_secret</literal> - a
+        string pointing to a file containing the value the option
+        should be set to. See the example to get a better picture of
+        this: in the resulting <filename>.env</filename> file, the
+        <literal>OIDC_CLIENT_SECRET</literal> key will be set to the
+        contents of the <filename>/run/keys/oidc_secret</filename>
+        file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = db.createLocally -> db.user == user;
+        message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
+      }
+      { assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
+      }
+    ];
+
+    environment.systemPackages = [ artisan ];
+
+    services.snipe-it.config = {
+      APP_ENV = "production";
+      APP_KEY._secret = cfg.appKeyFile;
+      APP_URL = cfg.appURL;
+      DB_HOST = db.host;
+      DB_PORT = db.port;
+      DB_DATABASE = db.name;
+      DB_USERNAME = db.user;
+      DB_PASSWORD._secret = db.passwordFile;
+      MAIL_DRIVER = mail.driver;
+      MAIL_FROM_NAME = mail.from.name;
+      MAIL_FROM_ADDR = mail.from.address;
+      MAIL_REPLYTO_NAME = mail.from.name;
+      MAIL_REPLYTO_ADDR = mail.from.address;
+      MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
+      MAIL_HOST = mail.host;
+      MAIL_PORT = mail.port;
+      MAIL_USERNAME = mail.user;
+      MAIL_ENCRYPTION = mail.encryption;
+      MAIL_PASSWORD._secret = mail.passwordFile;
+      APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
+      APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
+      APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
+      APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
+      APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
+      SESSION_SECURE_COOKIE = tlsEnabled;
+    };
+
+    services.mysql = mkIf db.createLocally {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ db.name ];
+      ensureUsers = [
+        { name = db.user;
+          ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.snipe-it = {
+      inherit user group;
+      phpPackage = pkgs.php81;
+      phpOptions = ''
+        post_max_size = ${cfg.maxUploadSize}
+        upload_max_filesize = ${cfg.maxUploadSize}
+      '';
+      settings = {
+        "listen.mode" = "0660";
+        "listen.owner" = user;
+        "listen.group" = group;
+      } // cfg.poolConfig;
+    };
+
+    services.nginx = {
+      enable = mkDefault true;
+      virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
+        root = mkForce "${snipe-it}/public";
+        extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
+        locations = {
+          "/" = {
+            index = "index.php";
+            extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
+          };
+          "~ \.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /index.php?$query_string;
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
+              ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
+            '';
+          };
+          "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+        };
+      }];
+    };
+
+    systemd.services.snipe-it-setup = {
+      description = "Preperation tasks for snipe-it";
+      before = [ "phpfpm-snipe-it.service" ];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = user;
+        WorkingDirectory = snipe-it;
+        RuntimeDirectory = "snipe-it/cache";
+        RuntimeDirectoryMode = 0700;
+      };
+      path = [ pkgs.replace-secret ];
+      script =
+        let
+          isSecret  = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
+          snipeITEnvVars = lib.generators.toKeyValue {
+            mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+              mkValueString = v: with builtins;
+                if isInt             v then toString v
+                else if isString     v then "\"${v}\""
+                else if true  ==     v then "true"
+                else if false ==     v then "false"
+                else if isSecret     v then
+                  if (isString v._secret) then
+                    hashString "sha256" v._secret
+                  else
+                    hashString "sha256" (builtins.readFile v._secret)
+                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+            };
+          };
+          secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
+          mkSecretReplacement = file: ''
+            replace-secret ${escapeShellArgs [
+              (
+                if (isString file) then
+                  builtins.hashString "sha256" file
+                else
+                  builtins.hashString "sha256" (builtins.readFile file)
+              )
+              file
+              "${cfg.dataDir}/.env"
+            ]}
+          '';
+          secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+          filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
+          snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
+        in ''
+          # error handling
+          set -euo pipefail
+
+          # set permissions
+          umask 077
+
+          # create .env file
+          install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
+
+          # replace secrets
+          ${secretReplacements}
+
+          # prepend `base64:` if it does not exist in APP_KEY
+          if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
+              sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
+          fi
+
+          # purge cache
+          rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
+
+          # migrate db
+          ${pkgs.php}/bin/php artisan migrate --force
+        '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap                  0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap/cache            0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public                     0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads             0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage                    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/app                0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/fonts              0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework          0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/cache    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/views    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/logs               0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/uploads            0700 ${user} ${group} - -"
+    ];
+
+    users = {
+      users = mkIf (user == "snipeit") {
+        snipeit = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [ group ];
+      };
+      groups = mkIf (group == "snipeit") {
+        snipeit = {};
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ yayayayaka ];
+}
diff --git a/nixos/modules/services/web-apps/timetagger.nix b/nixos/modules/services/web-apps/timetagger.nix
deleted file mode 100644
index 373f4fcd52f81..0000000000000
--- a/nixos/modules/services/web-apps/timetagger.nix
+++ /dev/null
@@ -1,80 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-let
-  inherit (lib) mkEnableOption mkIf mkOption types literalExpression;
-
-  cfg = config.services.timetagger;
-in {
-
-  options = {
-    services.timetagger = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Tag your time, get the insight
-
-          <note><para>
-            This app does not do authentication.
-            You must setup authentication yourself or run it in an environment where
-            only allowed users have access.
-          </para></note>
-        '';
-      };
-
-      bindAddr = mkOption {
-        description = "Address to bind to.";
-        type = types.str;
-        default = "127.0.0.1";
-      };
-
-      port = mkOption {
-        description = "Port to bind to.";
-        type = types.port;
-        default = 8080;
-      };
-
-      package = mkOption {
-        description = ''
-          Use own package for starting timetagger web application.
-
-          The ${literalExpression ''pkgs.timetagger''} package only provides a
-          "run.py" script for the actual package
-          ${literalExpression ''pkgs.python3Packages.timetagger''}.
-
-          If you want to provide a "run.py" script for starting timetagger
-          yourself, you can do so with this option.
-          If you do so, the 'bindAddr' and 'port' options are ignored.
-        '';
-
-        default = pkgs.timetagger.override { addr = cfg.bindAddr; port = cfg.port; };
-        defaultText = literalExpression ''
-          pkgs.timetagger.override {
-            addr = ${cfg.bindAddr};
-            port = ${cfg.port};
-          };
-        '';
-        type = types.package;
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.timetagger = {
-      description = "Timetagger service";
-      wantedBy = [ "multi-user.target" ];
-
-      serviceConfig = {
-        User = "timetagger";
-        Group = "timetagger";
-        StateDirectory = "timetagger";
-
-        ExecStart = "${cfg.package}/bin/timetagger";
-
-        Restart = "on-failure";
-        RestartSec = 1;
-      };
-    };
-  };
-}
-
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 9aa38ab25c9a3..c441a2a7764e2 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -534,6 +534,7 @@ let
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         inherit (cfg) user;
+        phpPackage = pkgs.php80;
         settings = mapAttrs (name: mkDefault) {
           "listen.owner" = "nginx";
           "listen.group" = "nginx";
diff --git a/nixos/modules/services/web-servers/agate.nix b/nixos/modules/services/web-servers/agate.nix
new file mode 100644
index 0000000000000..3afdb561c0b0a
--- /dev/null
+++ b/nixos/modules/services/web-servers/agate.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.agate;
+in
+{
+  options = {
+    services.agate = {
+      enable = mkEnableOption "Agate Server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.agate;
+        defaultText = literalExpression "pkgs.agate";
+        description = "The package to use";
+      };
+
+      addresses = mkOption {
+        type = types.listOf types.str;
+        default = [ "0.0.0.0:1965" ];
+        description = ''
+          Addresses to listen on, IP:PORT, if you haven't disabled forwarding
+          only set IPv4.
+        '';
+      };
+
+      contentDir = mkOption {
+        default = "/var/lib/agate/content";
+        type = types.path;
+        description = "Root of the content directory.";
+      };
+
+      certificatesDir = mkOption {
+        default = "/var/lib/agate/certificates";
+        type = types.path;
+        description = "Root of the certificate directory.";
+      };
+
+      hostnames = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = ''
+          Domain name of this Gemini server, enables checking hostname and port
+          in requests. (multiple occurences means basic vhosts)
+        '';
+      };
+
+      language = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = "RFC 4646 Language code for text/gemini documents.";
+      };
+
+      onlyTls_1_3 = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Only use TLSv1.3 (default also allows TLSv1.2).";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "" ];
+        example = [ "--log-ip" ];
+        description = "Extra arguments to use running agate.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # available for generating certs by hand
+    # it can be a bit arduous with openssl
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.agate = {
+      description = "Agate";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      script =
+        let
+          prefixKeyList = key: list: concatMap (v: [ key v ]) list;
+          addresses = prefixKeyList "--addr" cfg.addresses;
+          hostnames = prefixKeyList "--hostname" cfg.hostnames;
+        in
+        ''
+          exec ${cfg.package}/bin/agate ${
+            escapeShellArgs (
+              [
+                "--content" "${cfg.contentDir}"
+                "--certs" "${cfg.certificatesDir}"
+              ] ++
+              addresses ++
+              (optionals (cfg.hostnames != []) hostnames) ++
+              (optionals (cfg.language != null) [ "--lang" cfg.language ]) ++
+              (optionals cfg.onlyTls_1_3 [ "--only-tls13" ]) ++
+              (optionals (cfg.extraArgs != []) cfg.extraArgs)
+            )
+          }
+        '';
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        DynamicUser = true;
+        StateDirectory = "agate";
+
+        # Security options:
+        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;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@debug"
+          "~@keyring"
+          "~@memlock"
+          "~@obsolete"
+          "~@privileged"
+          "~@setuid"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index d817ff6019a3b..3099705acbe2e 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -710,20 +710,15 @@ in
 
     services.logrotate = optionalAttrs (cfg.logFormat != "none") {
       enable = mkDefault true;
-      paths.httpd = {
-        path = "${cfg.logDir}/*.log";
-        user = cfg.user;
-        group = cfg.group;
+      settings.httpd = {
+        files = "${cfg.logDir}/*.log";
+        su = "${cfg.user} ${cfg.group}";
         frequency = "daily";
-        keep = 28;
-        extraConfig = ''
-          sharedscripts
-          compress
-          delaycompress
-          postrotate
-            systemctl reload httpd.service > /dev/null 2>/dev/null || true
-          endscript
-        '';
+        rotate = 28;
+        sharedscripts = true;
+        compress = true;
+        delaycompress = true;
+        postrotate = "systemctl reload httpd.service > /dev/null 2>/dev/null || true";
       };
     };
 
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index 2b8c6f2e308bf..b262313577f10 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -299,7 +299,7 @@ in
         # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
         # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
         ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ];
-        ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ];
+        ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter} --force" ];
 
         ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}";
         User = cfg.user;
diff --git a/nixos/modules/services/web-servers/hydron.nix b/nixos/modules/services/web-servers/hydron.nix
index a4a5a435b2e6b..46f62a9119f0d 100644
--- a/nixos/modules/services/web-servers/hydron.nix
+++ b/nixos/modules/services/web-servers/hydron.nix
@@ -161,5 +161,5 @@ in with lib; {
     (mkRenamedOptionModule [ "services" "hydron" "baseDir" ] [ "services" "hydron" "dataDir" ])
   ];
 
-  meta.maintainers = with maintainers; [ chiiruno ];
+  meta.maintainers = with maintainers; [ Madouura ];
 }
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 6876dbf39d84b..5ccbaf77481b7 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -255,20 +255,22 @@ let
             else defaultListen;
 
         listenString = { addr, port, ssl, extraParameters ? [], ... }:
-          "listen ${addr}:${toString port} "
-          + optionalString ssl "ssl "
+          (if ssl && vhost.http3 then "
+          # UDP listener for **QUIC+HTTP/3
+          listen ${addr}:${toString port} http3 "
+          + optionalString vhost.default "default_server "
+          + optionalString vhost.reuseport "reuseport "
+          + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
+          + ";" else "")
+          + "
+
+            listen ${addr}:${toString port} "
           + optionalString (ssl && vhost.http2) "http2 "
+          + optionalString ssl "ssl "
           + optionalString vhost.default "default_server "
+          + optionalString vhost.reuseport "reuseport "
           + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
-          + ";"
-          + (if ssl && vhost.http3 then ''
-          # UDP listener for **QUIC+HTTP/3
-          listen ${addr}:${toString port} http3 reuseport;
-          # Advertise that HTTP/3 is available
-          add_header Alt-Svc 'h3=":443"';
-          # Sent when QUIC was used
-          add_header QUIC-Status $quic;
-          '' else "");
+          + ";";
 
         redirectListen = filter (x: !x.ssl) defaultListen;
 
@@ -321,6 +323,11 @@ let
             ssl_conf_command Options KTLS;
           ''}
 
+          ${optionalString (hasSSL && vhost.http3) ''
+            # Advertise that HTTP/3 is available
+            add_header Alt-Svc 'h3=":443"; ma=86400' always;
+          ''}
+
           ${mkBasicAuth vhostName vhost}
 
           ${mkLocations vhost.locations}
@@ -353,7 +360,7 @@ let
       ${optionalString (config.alias != null) "alias ${config.alias};"}
       ${optionalString (config.return != null) "return ${config.return};"}
       ${config.extraConfig}
-      ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
+      ${optionalString (config.proxyPass != null && config.recommendedProxySettings) "include ${recommendedProxyConfig};"}
       ${mkBasicAuth "sublocation" config}
     }
   '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
@@ -416,7 +423,7 @@ in
         default = false;
         type = types.bool;
         description = "
-          Enable recommended proxy settings.
+          Whether to enable recommended proxy settings if a vhost does not specify the option manually.
         ";
       };
 
@@ -924,7 +931,8 @@ in
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
-        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" ] ++ optionals (cfg.package != pkgs.tengine) [ "~@ipc" ];
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" ]
+          ++ optionals ((cfg.package != pkgs.tengine) && (cfg.package != pkgs.openresty) && (!lib.any (mod: (mod.disableIPC or false)) cfg.package.modules)) [ "~@ipc" ];
       };
     };
 
@@ -988,5 +996,14 @@ in
       nginx.gid = config.ids.gids.nginx;
     };
 
+    services.logrotate.settings.nginx = mapAttrs (_: mkDefault) {
+      files = "/var/log/nginx/*.log";
+      frequency = "weekly";
+      su = "${cfg.user} ${cfg.group}";
+      rotate = 26;
+      compress = true;
+      delaycompress = true;
+      postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`";
+    };
   };
 }
diff --git a/nixos/modules/services/web-servers/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix
index 11bf2a309ea81..db45577a46d16 100644
--- a/nixos/modules/services/web-servers/nginx/gitweb.nix
+++ b/nixos/modules/services/web-servers/nginx/gitweb.nix
@@ -79,7 +79,7 @@ in
         };
         locations."${cfg.location}/" = {
           extraConfig = ''
-            include ${pkgs.nginx}/conf/fastcgi_params;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
             fastcgi_param GITWEB_CONFIG ${gitwebConfig.gitwebConfigFile};
             fastcgi_pass unix:/run/gitweb/gitweb.sock;
           '';
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 6fd00b3869745..49dd8893015af 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -3,7 +3,7 @@
 # has additional options that affect the web server as a whole, like
 # the user/group to run under.)
 
-{ lib }:
+{ lib, config }:
 
 with lib;
 
@@ -128,5 +128,14 @@ with lib;
         a greater priority.
       '';
     };
+
+    recommendedProxySettings = mkOption {
+      type = types.bool;
+      default = config.services.nginx.recommendedProxySettings;
+      defaultText = literalExpression "config.services.nginx.recommendedProxySettings";
+      description = ''
+        Enable recommended proxy settings.
+      '';
+    };
   };
 }
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index c4e8285dc48bd..61eef9f7ac96e 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -20,7 +20,7 @@ with lib;
     serverAliases = mkOption {
       type = types.listOf types.str;
       default = [];
-      example = ["www.example.org" "example.org"];
+      example = [ "www.example.org" "example.org" ];
       description = ''
         Additional names of virtual hosts served by this virtual host configuration.
       '';
@@ -31,11 +31,11 @@ with lib;
         addr = mkOption { type = str;  description = "IP address.";  };
         port = mkOption { type = int;  description = "Port number."; default = 80; };
         ssl  = mkOption { type = bool; description = "Enable SSL.";  default = false; };
-        extraParameters = mkOption { type = listOf str; description = "Extra parameters of this listen directive."; default = []; example = [ "reuseport" "deferred" ]; };
+        extraParameters = mkOption { type = listOf str; description = "Extra parameters of this listen directive."; default = []; example = [ "backlog=1024" "deferred" ]; };
       }; });
       default = [];
       example = [
-        { addr = "195.154.1.1"; port = 443; ssl = true;}
+        { addr = "195.154.1.1"; port = 443; ssl = true; }
         { addr = "192.154.1.1"; port = 80; }
       ];
       description = ''
@@ -60,7 +60,7 @@ with lib;
         Note: This option overrides <literal>enableIPv6</literal>
       '';
       default = [];
-      example = [ "127.0.0.1" "::1" ];
+      example = [ "127.0.0.1" "[::1]" ];
     };
 
     enableACME = mkOption {
@@ -207,6 +207,15 @@ with lib;
       '';
     };
 
+    reuseport = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Create an individual listening socket .
+        It is required to specify only once on one of the hosts.
+      '';
+    };
+
     root = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -272,7 +281,7 @@ with lib;
 
     locations = mkOption {
       type = types.attrsOf (types.submodule (import ./location-options.nix {
-        inherit lib;
+        inherit lib config;
       }));
       default = {};
       example = literalExpression ''
diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix
index 2bc7d01c7c287..0b460755f50ef 100644
--- a/nixos/modules/services/web-servers/pomerium.nix
+++ b/nixos/modules/services/web-servers/pomerium.nix
@@ -69,11 +69,16 @@ in
         CERTIFICATE_KEY_FILE = "key.pem";
       };
       startLimitIntervalSec = 60;
+      script = ''
+        if [[ -v CREDENTIALS_DIRECTORY ]]; then
+          cd "$CREDENTIALS_DIRECTORY"
+        fi
+        exec "${pkgs.pomerium}/bin/pomerium" -config "${cfgFile}"
+      '';
 
       serviceConfig = {
         DynamicUser = true;
         StateDirectory = [ "pomerium" ];
-        ExecStart = "${pkgs.pomerium}/bin/pomerium -config ${cfgFile}";
 
         PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
         MemoryDenyWriteExecute = false;  # breaks LuaJIT
@@ -99,7 +104,6 @@ in
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
 
-        WorkingDirectory = mkIf (cfg.useACMEHost != null) "$CREDENTIALS_DIRECTORY";
         LoadCredential = optionals (cfg.useACMEHost != null) [
           "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem"
           "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem"
@@ -124,7 +128,7 @@ in
         Type = "oneshot";
         TimeoutSec = 60;
         ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service";
-        ExecStart = "/run/current-system/systemd/bin/systemctl restart pomerium.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl --no-block restart pomerium.service";
       };
     };
   });
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index f9446fe125a34..877097cf37813 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -23,8 +23,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.tomcat85;
-        defaultText = literalExpression "pkgs.tomcat85";
+        default = pkgs.tomcat9;
+        defaultText = literalExpression "pkgs.tomcat9";
         example = lib.literalExpression "pkgs.tomcat9";
         description = ''
           Which tomcat package to use.
@@ -127,7 +127,7 @@ in
       webapps = mkOption {
         type = types.listOf types.path;
         default = [ tomcat.webapps ];
-        defaultText = literalExpression "[ pkgs.tomcat85.webapps ]";
+        defaultText = literalExpression "[ config.services.tomcat.package.webapps ]";
         description = "List containing WAR files or directories with WAR files which are web applications to be deployed on Tomcat";
       };
 
@@ -201,6 +201,7 @@ in
       { uid = config.ids.uids.tomcat;
         description = "Tomcat user";
         home = "/homeless-shelter";
+        group = "tomcat";
         extraGroups = cfg.extraGroups;
       };
 
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index 3a78a526460c1..705dbec5e7428 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -196,7 +196,7 @@ in
       programs.evince.enable = mkDefault true;
       programs.file-roller.enable = mkDefault true;
 
-      environment.systemPackages = (with pkgs // pkgs.gnome // pkgs.cinnamon; pkgs.gnome.removePackagesByName [
+      environment.systemPackages = with pkgs // pkgs.gnome // pkgs.cinnamon; utils.removePackagesByName [
         # cinnamon team apps
         bulky
         blueberry
@@ -212,7 +212,7 @@ in
         # external apps shipped with linux-mint
         hexchat
         gnome-calculator
-      ] config.environment.cinnamon.excludePackages);
+      ] config.environment.cinnamon.excludePackages;
     })
   ];
 }
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 8247a7e381c90..ffdf7e9a86eb5 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -18,7 +18,7 @@ in
   # determines the default: later modules (if enabled) are preferred.
   # E.g., if Plasma 5 is enabled, it supersedes xterm.
   imports = [
-    ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
+    ./none.nix ./xterm.nix ./phosh.nix ./xfce.nix ./plasma5.nix ./lumina.nix
     ./lxqt.nix ./enlightenment.nix ./gnome.nix ./retroarch.nix ./kodi.nix
     ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
     ./cinnamon.nix
@@ -72,7 +72,9 @@ in
         apply = map (d: d // {
           manage = "desktop";
           start = d.start
+          # literal newline to ensure d.start's last line is not appended to
           + optionalString (needBGCond d) ''
+
             if [ -e $HOME/.background-image ]; then
               ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
             fi
diff --git a/nixos/modules/services/x11/desktop-managers/enlightenment.nix b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
index d1513a596b9fa..991616bd192d5 100644
--- a/nixos/modules/services/x11/desktop-managers/enlightenment.nix
+++ b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
@@ -16,6 +16,10 @@ let
 in
 
 {
+  meta = {
+    maintainers = teams.enlightenment.members;
+  };
+
   imports = [
     (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "e19" "enable" ] [ "services" "xserver" "desktopManager" "enlightenment" "enable" ])
   ];
@@ -92,6 +96,7 @@ in
 
     services.udisks2.enable = true;
     services.upower.enable = config.powerManagement.enable;
+    services.xserver.libinput.enable = mkDefault true;
 
     services.dbus.packages = [ e.efl ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.nix b/nixos/modules/services/x11/desktop-managers/gnome.nix
index e2323785149a9..ff9d08ea99701 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -22,6 +22,9 @@ let
     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-background-ligtht = pkgs.nixos-artwork.wallpapers.simple-blue;
+  nixos-background-dark = pkgs.nixos-artwork.wallpapers.simple-dark-gray;
+
   nixos-gsettings-desktop-schemas = let
     defaultPackages = with pkgs; [ gsettings-desktop-schemas gnome.gnome-shell ];
   in
@@ -42,10 +45,11 @@ let
      chmod -R a+w $out/share/gsettings-schemas/nixos-gsettings-overrides
      cat - > $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/nixos-defaults.gschema.override <<- EOF
        [org.gnome.desktop.background]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray.gnomeFilePath}'
+       picture-uri='file://${nixos-background-ligtht.gnomeFilePath}'
+       picture-uri-dark='file://${nixos-background-dark.gnomeFilePath}'
 
        [org.gnome.desktop.screensaver]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath}'
+       picture-uri='file://${nixos-background-dark.gnomeFilePath}'
 
        ${cfg.favoriteAppsOverride}
 
@@ -55,6 +59,26 @@ let
      ${pkgs.glib.dev}/bin/glib-compile-schemas $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/
     '';
 
+  nixos-background-info = pkgs.writeTextFile rec {
+    name = "nixos-background-info";
+    text = ''
+      <?xml version="1.0"?>
+      <!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
+      <wallpapers>
+        <wallpaper deleted="false">
+          <name>Blobs</name>
+          <filename>${nixos-background-ligtht.gnomeFilePath}</filename>
+          <filename-dark>${nixos-background-dark.gnomeFilePath}</filename-dark>
+          <options>zoom</options>
+          <shade_type>solid</shade_type>
+          <pcolor>#3a4ba0</pcolor>
+          <scolor>#2f302f</scolor>
+        </wallpaper>
+      </wallpapers>
+    '';
+    destination = "/share/gnome-background-properties/nixos.xml";
+  };
+
   flashbackEnabled = cfg.flashback.enableMetacity || length cfg.flashback.customSessions > 0;
   flashbackWms = optional cfg.flashback.enableMetacity {
     wmName = "metacity";
@@ -132,6 +156,10 @@ in
       [ "environment" "gnome3" "excludePackages" ]
       [ "environment" "gnome" "excludePackages" ]
     )
+    (mkRemovedOptionModule
+      [ "services" "gnome" "experimental-features" "realtime-scheduling" ]
+      "Set `security.rtkit.enable = true;` to make realtime scheduling possible. (Still needs to be enabled using GSettings.)"
+    )
   ];
 
   options = {
@@ -142,38 +170,6 @@ in
       core-utilities.enable = mkEnableOption "GNOME core utilities";
       core-developer-tools.enable = mkEnableOption "GNOME core developer tools";
       games.enable = mkEnableOption "GNOME games";
-
-      experimental-features = {
-        realtime-scheduling = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Makes mutter (which propagates to gnome-shell) request a low priority real-time
-            scheduling which is only available on the wayland session.
-            To enable this experimental feature it requires a restart of the compositor.
-            Note that enabling this option only enables the <emphasis>capability</emphasis>
-            for realtime-scheduling to be used. It doesn't automatically set the gsetting
-            so that mutter actually uses realtime-scheduling. This would require adding <literal>
-            rt-scheduler</literal> to <literal>/org/gnome/mutter/experimental-features</literal>
-            with dconf-editor. You cannot use extraGSettingsOverrides because that will only
-            change the default value of the setting.
-
-            Please be aware of these known issues with the feature in nixos:
-            <itemizedlist>
-             <listitem>
-              <para>
-               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/90201">NixOS/nixpkgs#90201</link>
-              </para>
-             </listitem>
-             <listitem>
-              <para>
-               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/86730">NixOS/nixpkgs#86730</link>
-              </para>
-            </listitem>
-            </itemizedlist>
-          '';
-        };
-      };
     };
 
     services.xserver.desktopManager.gnome = {
@@ -193,7 +189,6 @@ in
 
           Note that this should be a last resort; patching the package is preferred (see GPaste).
         '';
-        apply = list: list ++ [ pkgs.gnome.gnome-shell pkgs.gnome.gnome-shell-extensions ];
       };
 
       favoriteAppsOverride = mkOption {
@@ -371,6 +366,10 @@ in
       services.upower.enable = config.powerManagement.enable;
       services.xserver.libinput.enable = mkDefault true; # for controlling touchpad settings via gnome control center
 
+      # Explicitly enabled since GNOME will be severely broken without these.
+      xdg.mime.enable = true;
+      xdg.icons.enable = true;
+
       xdg.portal.enable = true;
       xdg.portal.extraPortals = [
         pkgs.xdg-desktop-portal-gnome
@@ -404,6 +403,18 @@ in
     })
 
     (mkIf serviceCfg.core-shell.enable {
+      services.xserver.desktopManager.gnome.sessionPath =
+        let
+          mandatoryPackages = [
+            pkgs.gnome.gnome-shell
+          ];
+          optionalPackages = [
+            pkgs.gnome.gnome-shell-extensions
+          ];
+        in
+        mandatoryPackages
+        ++ utils.removePackagesByName optionalPackages config.environment.gnome.excludePackages;
+
       services.colord.enable = mkDefault true;
       services.gnome.chrome-gnome-shell.enable = mkDefault true;
       services.gnome.glib-networking.enable = true;
@@ -414,7 +425,6 @@ in
       services.gnome.rygel.enable = mkDefault true;
       services.gvfs.enable = true;
       services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
-      services.telepathy.enable = mkDefault true;
 
       systemd.packages = with pkgs.gnome; [
         gnome-session
@@ -457,74 +467,55 @@ in
       ];
 
       # 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.gnome; [
-        adwaita-icon-theme
-        gnome-backgrounds
-        gnome-bluetooth
-        gnome-color-manager
-        gnome-control-center
-        gnome-shell
-        gnome-shell-extensions
-        gnome-themes-extra
-        pkgs.gnome-tour # GNOME Shell detects the .desktop file on first log-in.
-        pkgs.nixos-artwork.wallpapers.simple-dark-gray
-        pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom
-        pkgs.gnome-user-docs
-        pkgs.orca
-        pkgs.glib # for gsettings
-        pkgs.gnome-menus
-        pkgs.gtk3.out # for gtk-launch
-        pkgs.hicolor-icon-theme
-        pkgs.shared-mime-info # for update-mime-database
-        pkgs.xdg-user-dirs # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
-      ];
-    })
-
-    # Enable soft realtime scheduling, only supported on wayland
-    (mkIf serviceCfg.experimental-features.realtime-scheduling {
-      security.wrappers.".gnome-shell-wrapped" = {
-        source = "${pkgs.gnome.gnome-shell}/bin/.gnome-shell-wrapped";
-        owner = "root";
-        group = "root";
-        capabilities = "cap_sys_nice=ep";
-      };
-
-      systemd.user.services.gnome-shell-wayland = let
-        gnomeShellRT = with pkgs.gnome; pkgs.runCommand "gnome-shell-rt" {} ''
-          mkdir -p $out/bin/
-          cp ${gnome-shell}/bin/gnome-shell $out/bin
-          sed -i "s@${gnome-shell}/bin/@${config.security.wrapperDir}/@" $out/bin/gnome-shell
-        '';
-      in {
-        # Note we need to clear ExecStart before overriding it
-        serviceConfig.ExecStart = ["" "${gnomeShellRT}/bin/gnome-shell"];
-        # Do not use the default environment, it provides a broken PATH
-        environment = mkForce {};
-      };
+      environment.systemPackages =
+        let
+          mandatoryPackages = with pkgs.gnome; [
+            gnome-shell
+          ];
+          optionalPackages = with pkgs.gnome; [
+            adwaita-icon-theme
+            nixos-background-info
+            gnome-backgrounds
+            gnome-bluetooth
+            gnome-color-manager
+            gnome-control-center
+            gnome-shell-extensions
+            gnome-themes-extra
+            pkgs.gnome-tour # GNOME Shell detects the .desktop file on first log-in.
+            pkgs.gnome-user-docs
+            pkgs.orca
+            pkgs.glib # for gsettings program
+            pkgs.gnome-menus
+            pkgs.gtk3.out # for gtk-launch program
+            pkgs.xdg-user-dirs # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
+          ];
+        in
+        mandatoryPackages
+        ++ utils.removePackagesByName optionalPackages config.environment.gnome.excludePackages;
     })
 
     # 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.gnome;
-        removePackagesByName
+        utils.removePackagesByName
           ([
             baobab
             cheese
             eog
             epiphany
-            gedit
+            pkgs.gnome-text-editor
             gnome-calculator
             gnome-calendar
             gnome-characters
             gnome-clocks
+            pkgs.gnome-console
             gnome-contacts
             gnome-font-viewer
             gnome-logs
             gnome-maps
             gnome-music
             pkgs.gnome-photos
-            gnome-screenshot
             gnome-system-monitor
             gnome-weather
             nautilus
@@ -547,10 +538,13 @@ in
       programs.file-roller.enable = notExcluded pkgs.gnome.file-roller;
       programs.geary.enable = notExcluded pkgs.gnome.geary;
       programs.gnome-disks.enable = notExcluded pkgs.gnome.gnome-disk-utility;
-      programs.gnome-terminal.enable = notExcluded pkgs.gnome.gnome-terminal;
       programs.seahorse.enable = notExcluded pkgs.gnome.seahorse;
       services.gnome.sushi.enable = notExcluded pkgs.gnome.sushi;
 
+      # VTE shell integration for gnome-console
+      programs.bash.vteIntegration = mkDefault true;
+      programs.zsh.vteIntegration = mkDefault true;
+
       # Let nautilus find extensions
       # TODO: Create nautilus-with-extensions package
       environment.sessionVariables.NAUTILUS_EXTENSION_DIR = "${config.system.path}/lib/nautilus/extensions-3.0";
@@ -564,7 +558,7 @@ in
     })
 
     (mkIf serviceCfg.games.enable {
-      environment.systemPackages = (with pkgs.gnome; removePackagesByName [
+      environment.systemPackages = with pkgs.gnome; utils.removePackagesByName [
         aisleriot
         atomix
         five-or-more
@@ -585,12 +579,12 @@ in
         quadrapassel
         swell-foop
         tali
-      ] config.environment.gnome.excludePackages);
+      ] config.environment.gnome.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.gnome; removePackagesByName [
+      environment.systemPackages = with pkgs.gnome; utils.removePackagesByName [
         dconf-editor
         devhelp
         pkgs.gnome-builder
@@ -599,7 +593,7 @@ in
         # in default configurations.
         # https://github.com/NixOS/nixpkgs/issues/60908
         /* gnome-boxes */
-      ] config.environment.gnome.excludePackages);
+      ] config.environment.gnome.excludePackages;
 
       services.sysprof.enable = notExcluded pkgs.sysprof;
     })
diff --git a/nixos/modules/services/x11/desktop-managers/lumina.nix b/nixos/modules/services/x11/desktop-managers/lumina.nix
index 419f5055d8be9..faa83b8bc54a5 100644
--- a/nixos/modules/services/x11/desktop-managers/lumina.nix
+++ b/nixos/modules/services/x11/desktop-managers/lumina.nix
@@ -10,6 +10,10 @@ let
 in
 
 {
+  meta = {
+    maintainers = teams.lumina.members;
+  };
+
   options = {
 
     services.xserver.desktopManager.lumina.enable = mkOption {
diff --git a/nixos/modules/services/x11/desktop-managers/lxqt.nix b/nixos/modules/services/x11/desktop-managers/lxqt.nix
index 720985ba0d94e..2aa9e5ab331de 100644
--- a/nixos/modules/services/x11/desktop-managers/lxqt.nix
+++ b/nixos/modules/services/x11/desktop-managers/lxqt.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -9,6 +9,10 @@ let
 in
 
 {
+  meta = {
+    maintainers = teams.lxqt.members;
+  };
+
   options = {
 
     services.xserver.desktopManager.lxqt.enable = mkOption {
@@ -51,7 +55,7 @@ in
     environment.systemPackages =
       pkgs.lxqt.preRequisitePackages ++
       pkgs.lxqt.corePackages ++
-      (pkgs.gnome.removePackagesByName
+      (utils.removePackagesByName
         pkgs.lxqt.optionalPackages
         config.environment.lxqt.excludePackages);
 
@@ -62,6 +66,10 @@ in
     services.gvfs.enable = true;
 
     services.upower.enable = config.powerManagement.enable;
+
+    services.xserver.libinput.enable = mkDefault true;
+
+    xdg.portal.lxqt.enable = true;
   };
 
 }
diff --git a/nixos/modules/services/x11/desktop-managers/mate.nix b/nixos/modules/services/x11/desktop-managers/mate.nix
index f8f47a0614524..b63510475ec5f 100644
--- a/nixos/modules/services/x11/desktop-managers/mate.nix
+++ b/nixos/modules/services/x11/desktop-managers/mate.nix
@@ -1,20 +1,9 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
 let
 
-  addToXDGDirs = p: ''
-    if [ -d "${p}/share/gsettings-schemas/${p.name}" ]; then
-      export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}${p}/share/gsettings-schemas/${p.name}
-    fi
-
-    if [ -d "${p}/lib/girepository-1.0" ]; then
-      export GI_TYPELIB_PATH=$GI_TYPELIB_PATH''${GI_TYPELIB_PATH:+:}${p}/lib/girepository-1.0
-      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH''${LD_LIBRARY_PATH:+:}${p}/lib
-    fi
-  '';
-
   xcfg = config.services.xserver;
   cfg = xcfg.desktopManager.mate;
 
@@ -48,24 +37,8 @@ in
       pkgs.mate.mate-session-manager
     ];
 
-    services.xserver.displayManager.sessionCommands = ''
-      if test "$XDG_CURRENT_DESKTOP" = "MATE"; then
-          export XDG_MENU_PREFIX=mate-
-
-          # Let caja find extensions
-          export CAJA_EXTENSION_DIRS=$CAJA_EXTENSION_DIRS''${CAJA_EXTENSION_DIRS:+:}${config.system.path}/lib/caja/extensions-2.0
-
-          # Let caja extensions find gsettings schemas
-          ${concatMapStrings (p: ''
-          if [ -d "${p}/lib/caja/extensions-2.0" ]; then
-              ${addToXDGDirs p}
-          fi
-          '') config.environment.systemPackages}
-
-          # Add mate-control-center paths to some XDG variables because its schemas are needed by mate-settings-daemon, and mate-settings-daemon is a dependency for mate-control-center (that is, they are mutually recursive)
-          ${addToXDGDirs pkgs.mate.mate-control-center}
-      fi
-    '';
+    # Let caja find extensions
+    environment.sessionVariables.CAJA_EXTENSION_DIRS = [ "${config.system.path}/lib/caja/extensions-2.0" ];
 
     # Let mate-panel find applets
     environment.sessionVariables."MATE_PANEL_APPLETS_DIR" = "${config.system.path}/share/mate-panel/applets";
@@ -74,20 +47,18 @@ in
     # Debugging
     environment.sessionVariables.MATE_SESSION_DEBUG = mkIf cfg.debug "1";
 
-    environment.systemPackages =
-      pkgs.mate.basePackages ++
-      (pkgs.gnome.removePackagesByName
-        pkgs.mate.extraPackages
-        config.environment.mate.excludePackages) ++
+    environment.systemPackages = utils.removePackagesByName
+      (pkgs.mate.basePackages ++
+      pkgs.mate.extraPackages ++
       [
         pkgs.desktop-file-utils
         pkgs.glib
         pkgs.gtk3.out
         pkgs.shared-mime-info
         pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
-        pkgs.mate.mate-settings-daemon
         pkgs.yelp # for 'Contents' in 'Help' menus
-      ];
+      ])
+      config.environment.mate.excludePackages;
 
     programs.dconf.enable = true;
     # Shell integration for VTE terminals
@@ -102,6 +73,7 @@ in
     services.udev.packages = [ pkgs.mate.mate-settings-daemon ];
     services.gvfs.enable = true;
     services.upower.enable = config.powerManagement.enable;
+    services.xserver.libinput.enable = mkDefault true;
 
     security.pam.services.mate-screensaver.unixAuth = true;
 
diff --git a/nixos/modules/services/x11/desktop-managers/none.nix b/nixos/modules/services/x11/desktop-managers/none.nix
index af7a376ae0296..b5e498b67a018 100644
--- a/nixos/modules/services/x11/desktop-managers/none.nix
+++ b/nixos/modules/services/x11/desktop-managers/none.nix
@@ -1,7 +1,46 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  runXdgAutostart = config.services.xserver.desktopManager.runXdgAutostartIfNone;
+in
 {
-  services.xserver.desktopManager.session =
-    [ { name = "none";
-        start = "";
-      }
-    ];
+  options = {
+    services.xserver.desktopManager.runXdgAutostartIfNone = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to run XDG autostart files for sessions without a desktop manager
+        (with only a window manager), these sessions usually don't handle XDG
+        autostart files by default.
+
+        Some services like <option>i18n.inputMethod</option> and
+        <option>service.earlyoom</option> use XDG autostart files to start.
+        If this option is not set to <literal>true</literal> and you are using
+        a window manager without a desktop manager, you need to manually start
+        them or running <package>dex</package> somewhere.
+      '';
+    };
+  };
+
+  config = mkMerge [
+    {
+      services.xserver.desktopManager.session = [
+        {
+          name = "none";
+          start = optionalString runXdgAutostart ''
+            /run/current-system/systemd/bin/systemctl --user start xdg-autostart-if-no-desktop-manager.target
+          '';
+        }
+      ];
+    }
+    (mkIf runXdgAutostart {
+      systemd.user.targets.xdg-autostart-if-no-desktop-manager = {
+        description = "Run XDG autostart files";
+        # From `plasma-workspace`, `share/systemd/user/plasma-workspace@.target`.
+        requires = [ "xdg-desktop-autostart.target" "graphical-session.target" ];
+        before = [ "xdg-desktop-autostart.target" "graphical-session.target" ];
+        bindsTo = [ "graphical-session.target" ];
+      };
+    })
+  ];
 }
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 6a7d2a8aa6cd9..d04e565f7d310 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, utils, pkgs, ... }:
 
 with lib;
 
@@ -50,10 +50,6 @@ in
 
           Note that this should be a last resort; patching the package is preferred (see GPaste).
         '';
-        apply = list: list ++
-        [
-          pkgs.pantheon.pantheon-agent-geoclue2
-        ];
       };
 
       extraWingpanelIndicators = mkOption {
@@ -96,6 +92,9 @@ in
 
   config = mkMerge [
     (mkIf cfg.enable {
+      services.xserver.desktopManager.pantheon.sessionPath = utils.removePackagesByName [
+        pkgs.pantheon.pantheon-agent-geoclue2
+      ] config.environment.pantheon.excludePackages;
 
       services.xserver.displayManager.sessionPackages = [ pkgs.pantheon.elementary-session-settings ];
 
@@ -136,6 +135,7 @@ in
       services.colord.enable = mkDefault true;
       services.fwupd.enable = mkDefault true;
       services.packagekit.enable = mkDefault true;
+      services.power-profiles-daemon.enable = mkDefault true;
       services.touchegg.enable = mkDefault true;
       services.touchegg.package = pkgs.pantheon.touchegg;
       services.tumbler.enable = mkDefault true;
@@ -167,28 +167,37 @@ in
         isSystem = true;
       };
       services.udev.packages = [
-        pkgs.gnome.gnome-settings-daemon338
+        pkgs.pantheon.gnome-settings-daemon
       ];
       systemd.packages = [
-        pkgs.gnome.gnome-settings-daemon338
+        pkgs.pantheon.gnome-settings-daemon
       ];
       programs.dconf.enable = true;
       networking.networkmanager.enable = mkDefault true;
 
       # Global environment
-      environment.systemPackages = with pkgs; [
+      environment.systemPackages = (with pkgs.pantheon; [
+        elementary-session-settings
+        elementary-settings-daemon
+        gala
+        gnome-settings-daemon
+        (switchboard-with-plugs.override {
+          plugs = cfg.extraSwitchboardPlugs;
+        })
+        (wingpanel-with-indicators.override {
+          indicators = cfg.extraWingpanelIndicators;
+        })
+      ]) ++ utils.removePackagesByName ((with pkgs; [
         desktop-file-utils
-        glib
+        glib # for gsettings program
         gnome-menus
         gnome.adwaita-icon-theme
-        gtk3.out
-        hicolor-icon-theme
+        gtk3.out # for gtk-launch program
         onboard
         qgnomeplatform
-        shared-mime-info
         sound-theme-freedesktop
-        xdg-user-dirs
-      ] ++ (with pkgs.pantheon; [
+        xdg-user-dirs # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
+      ]) ++ (with pkgs.pantheon; [
         # Artwork
         elementary-gtk-theme
         elementary-icon-theme
@@ -198,35 +207,22 @@ in
         # Desktop
         elementary-default-settings
         elementary-dock
-        elementary-session-settings
         elementary-shortcut-overlay
-        gala
-        (switchboard-with-plugs.override {
-          plugs = cfg.extraSwitchboardPlugs;
-        })
-        (wingpanel-with-indicators.override {
-          indicators = cfg.extraWingpanelIndicators;
-        })
 
         # Services
         elementary-capnet-assist
         elementary-notifications
-        elementary-settings-daemon
         pantheon-agent-geoclue2
         pantheon-agent-polkit
-      ]) ++ (gnome.removePackagesByName [
-        gnome.gnome-font-viewer
-        gnome.gnome-settings-daemon338
-      ] config.environment.pantheon.excludePackages);
-
-      programs.evince.enable = mkDefault true;
-      programs.evince.package = pkgs.pantheon.evince;
-      programs.file-roller.enable = mkDefault true;
-      programs.file-roller.package = pkgs.pantheon.file-roller;
+      ])) config.environment.pantheon.excludePackages;
 
       # Settings from elementary-default-settings
       environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
 
+      xdg.mime.enable = true;
+      xdg.icons.enable = true;
+
+      xdg.portal.enable = true;
       xdg.portal.extraPortals = with pkgs.pantheon; [
         elementary-files
         elementary-settings-daemon
@@ -273,7 +269,12 @@ in
     })
 
     (mkIf serviceCfg.apps.enable {
-      environment.systemPackages = with pkgs.pantheon; pkgs.gnome.removePackagesByName ([
+      programs.evince.enable = mkDefault true;
+      programs.file-roller.enable = mkDefault true;
+
+      environment.systemPackages = utils.removePackagesByName ([
+        pkgs.gnome.gnome-font-viewer
+      ] ++ (with pkgs.pantheon; [
         elementary-calculator
         elementary-calendar
         elementary-camera
@@ -291,7 +292,8 @@ in
         # Only install appcenter if flatpak is enabled before
         # https://github.com/NixOS/nixpkgs/issues/15932 is resolved.
         appcenter
-      ]) config.environment.pantheon.excludePackages;
+        sideload
+      ])) config.environment.pantheon.excludePackages;
 
       # needed by screenshot
       fonts.fonts = [
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
index 202909d398f08..6226f8f6a272f 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.xml
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.xml
@@ -3,7 +3,7 @@
          xml:id="chap-pantheon">
  <title>Pantheon Desktop</title>
  <para>
-  Pantheon is the desktop environment created for the elementary OS distribution. It is written from scratch in Vala, utilizing GNOME technologies with GTK 3 and Granite.
+  Pantheon is the desktop environment created for the elementary OS distribution. It is written from scratch in Vala, utilizing GNOME technologies with GTK and Granite.
  </para>
  <section xml:id="sec-pantheon-enable">
   <title>Enabling Pantheon</title>
@@ -89,9 +89,9 @@ switchboard-with-plugs.override {
      </para>
     </listitem>
    </varlistentry>
-   <varlistentry xml:id="sec-pantheon-faq-gnome3-and-pantheon">
+   <varlistentry xml:id="sec-pantheon-faq-gnome-and-pantheon">
     <term>
-     I cannot enable both GNOME 3 and Pantheon.
+     I cannot enable both GNOME and Pantheon.
     </term>
     <listitem>
      <para>
diff --git a/nixos/modules/programs/phosh.nix b/nixos/modules/services/x11/desktop-managers/phosh.nix
index cba3f73768ec9..5efe645d8aa24 100644
--- a/nixos/modules/programs/phosh.nix
+++ b/nixos/modules/services/x11/desktop-managers/phosh.nix
@@ -3,23 +3,22 @@
 with lib;
 
 let
-  cfg = config.programs.phosh;
+  cfg = config.services.xserver.desktopManager.phosh;
 
   # Based on https://source.puri.sm/Librem5/librem5-base/-/blob/4596c1056dd75ac7f043aede07887990fd46f572/default/sm.puri.OSK0.desktop
   oskItem = pkgs.makeDesktopItem {
     name = "sm.puri.OSK0";
-    type = "Application";
     desktopName = "On-screen keyboard";
     exec = "${pkgs.squeekboard}/bin/squeekboard";
-    categories = "GNOME;Core;";
-    extraEntries = ''
-      OnlyShowIn=GNOME;
-      NoDisplay=true
-      X-GNOME-Autostart-Phase=Panel
-      X-GNOME-Provides=inputmethod
-      X-GNOME-Autostart-Notify=true
-      X-GNOME-AutoRestart=true
-    '';
+    categories = [ "GNOME" "Core" ];
+    onlyShowIn = [ "GNOME" ];
+    noDisplay = true;
+    extraConfig = {
+      X-GNOME-Autostart-Phase = "Panel";
+      X-GNOME-Provides = "inputmethod";
+      X-GNOME-Autostart-Notify = "true";
+      X-GNOME-AutoRestart = "true";
+    };
   };
 
   phocConfigType = types.submodule {
@@ -79,7 +78,13 @@ let
         description = ''
           Display scaling factor.
         '';
-        type = types.nullOr types.ints.unsigned;
+        type = types.nullOr (
+          types.addCheck
+          (types.either types.int types.float)
+          (x : x > 0)
+        ) // {
+          description = "null or positive integer or float";
+        };
         default = null;
         example = 2;
       };
@@ -119,12 +124,39 @@ let
     [cursor]
     theme = ${phoc.cursorTheme}
   '';
-in {
+in
+
+{
   options = {
-    programs.phosh = {
-      enable = mkEnableOption ''
-        Whether to enable, Phosh, related packages and default configurations.
-      '';
+    services.xserver.desktopManager.phosh = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the Phone Shell.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.phosh;
+        defaultText = literalExpression "pkgs.phosh";
+        example = literalExpression "pkgs.phosh";
+        description = ''
+          Package that should be used for Phosh.
+        '';
+      };
+
+      user = mkOption {
+        description = "The user to run the Phosh service.";
+        type = types.str;
+        example = "alice";
+      };
+
+      group = mkOption {
+        description = "The group to run the Phosh service.";
+        type = types.str;
+        example = "users";
+      };
+
       phocConfig = mkOption {
         description = ''
           Configurations for the Phoc compositor.
@@ -136,14 +168,42 @@ in {
   };
 
   config = mkIf cfg.enable {
+    systemd.defaultUnit = "graphical.target";
+    # Inspired by https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/data/phosh.service
+    systemd.services.phosh = {
+      wantedBy = [ "graphical.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/phosh";
+        User = cfg.user;
+        Group = cfg.group;
+        PAMName = "login";
+        WorkingDirectory = "~";
+        Restart = "always";
+
+        TTYPath = "/dev/tty7";
+        TTYReset = "yes";
+        TTYVHangup = "yes";
+        TTYVTDisallocate = "yes";
+
+        # Fail to start if not controlling the tty.
+        StandardInput = "tty-fail";
+        StandardOutput = "journal";
+        StandardError = "journal";
+
+        # Log this user with utmp, letting it show up with commands 'w' and 'who'.
+        UtmpIdentifier = "tty7";
+        UtmpMode = "user";
+      };
+    };
+
     environment.systemPackages = [
       pkgs.phoc
-      pkgs.phosh
+      cfg.package
       pkgs.squeekboard
       oskItem
     ];
 
-    systemd.packages = [ pkgs.phosh ];
+    systemd.packages = [ cfg.package ];
 
     programs.feedbackd.enable = true;
 
@@ -153,7 +213,7 @@ in {
 
     services.gnome.core-shell.enable = true;
     services.gnome.core-os-services.enable = true;
-    services.xserver.displayManager.sessionPackages = [ pkgs.phosh ];
+    services.xserver.displayManager.sessionPackages = [ cfg.package ];
 
     environment.etc."phosh/phoc.ini".source =
       if builtins.isPath cfg.phocConfig then cfg.phocConfig
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index b7aa2eba81cff..0d17d136798c8 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 let
   xcfg = config.services.xserver;
@@ -30,7 +30,7 @@ let
   inherit (libsForQt5) kdeGear kdeFrameworks plasma5;
   inherit (pkgs) writeText;
   inherit (lib)
-    getBin optionalString
+    getBin optionalString literalExpression
     mkRemovedOptionModule mkRenamedOptionModule
     mkDefault mkIf mkMerge mkOption types;
 
@@ -192,6 +192,13 @@ in
       default = false;
     };
 
+    excludePackages = mkOption {
+      description = "List of default packages to exclude from the configuration";
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ pkgs.plasma5Packages.oxygen ]";
+    };
+
     # Internally allows configuring kdeglobals globally
     kdeglobals = mkOption {
       internal = true;
@@ -234,11 +241,11 @@ in
     (mkIf (cfg.enable || cfg.mobile.enable) {
 
       security.wrappers = {
-        kcheckpass = {
+        kscreenlocker_greet = {
           setuid = true;
           owner = "root";
           group = "root";
-          source = "${getBin libsForQt5.kscreenlocker}/libexec/kcheckpass";
+          source = "${getBin libsForQt5.kscreenlocker}/libexec/kscreenlocker_greet";
         };
         start_kdeinit = {
           setuid = true;
@@ -263,89 +270,95 @@ in
       environment.systemPackages =
         with libsForQt5;
         with plasma5; with kdeGear; with kdeFrameworks;
-        [
-          frameworkintegration
-          kactivities
-          kauth
-          kcmutils
-          kconfig
-          kconfigwidgets
-          kcoreaddons
-          kdoctools
-          kdbusaddons
-          kdeclarative
-          kded
-          kdesu
-          kdnssd
-          kemoticons
-          kfilemetadata
-          kglobalaccel
-          kguiaddons
-          kiconthemes
-          kidletime
-          kimageformats
-          kinit
-          kirigami2 # In system profile for SDDM theme. TODO: wrapper.
-          kio
-          kjobwidgets
-          knewstuff
-          knotifications
-          knotifyconfig
-          kpackage
-          kparts
-          kpeople
-          krunner
-          kservice
-          ktextwidgets
-          kwallet
-          kwallet-pam
-          kwalletmanager
-          kwayland
-          kwayland-integration
-          kwidgetsaddons
-          kxmlgui
-          kxmlrpcclient
-          plasma-framework
-          solid
-          sonnet
-          threadweaver
-
-          breeze-qt5
-          kactivitymanagerd
-          kde-cli-tools
-          kdecoration
-          kdeplasma-addons
-          kgamma5
-          khotkeys
-          kscreen
-          kscreenlocker
-          kwayland
-          kwin
-          kwrited
-          libkscreen
-          libksysguard
-          milou
-          plasma-browser-integration
-          plasma-integration
-          polkit-kde-agent
-
-          plasma-desktop
-          plasma-workspace
-          plasma-workspace-wallpapers
-
-          konsole
-          oxygen
-
-          breeze-icons
-          pkgs.hicolor-icon-theme
-
-          kde-gtk-config
-          breeze-gtk
-
-          qtvirtualkeyboard
-
-          pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
-        ]
+        let
+          requiredPackages = [
+            frameworkintegration
+            kactivities
+            kauth
+            kcmutils
+            kconfig
+            kconfigwidgets
+            kcoreaddons
+            kdoctools
+            kdbusaddons
+            kdeclarative
+            kded
+            kdesu
+            kdnssd
+            kemoticons
+            kfilemetadata
+            kglobalaccel
+            kguiaddons
+            kiconthemes
+            kidletime
+            kimageformats
+            kinit
+            kirigami2 # In system profile for SDDM theme. TODO: wrapper.
+            kio
+            kjobwidgets
+            knewstuff
+            knotifications
+            knotifyconfig
+            kpackage
+            kparts
+            kpeople
+            krunner
+            kservice
+            ktextwidgets
+            kwallet
+            kwallet-pam
+            kwalletmanager
+            kwayland
+            kwayland-integration
+            kwidgetsaddons
+            kxmlgui
+            kxmlrpcclient
+            plasma-framework
+            solid
+            sonnet
+            threadweaver
+
+            breeze-qt5
+            kactivitymanagerd
+            kde-cli-tools
+            kdecoration
+            kdeplasma-addons
+            kgamma5
+            khotkeys
+            kscreen
+            kscreenlocker
+            kwayland
+            kwin
+            kwrited
+            libkscreen
+            libksysguard
+            milou
+            plasma-integration
+            polkit-kde-agent
+
+            plasma-desktop
+            plasma-workspace
+            plasma-workspace-wallpapers
+
+            breeze-icons
+            pkgs.hicolor-icon-theme
+
+            kde-gtk-config
+            breeze-gtk
+
+            qtvirtualkeyboard
+
+            pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+          ];
+          optionalPackages = [
+            plasma-browser-integration
+            konsole
+            oxygen
+            (lib.getBin qttools) # Expose qdbus in PATH
+          ];
+        in
+        requiredPackages
+        ++ utils.removePackagesByName optionalPackages cfg.excludePackages
 
         # Phonon audio backend
         ++ lib.optional (cfg.phononBackend == "gstreamer") libsForQt5.phonon-backend-gstreamer
@@ -387,9 +400,10 @@ in
       services.accounts-daemon.enable = true;
       # when changing an account picture the accounts-daemon reads a temporary file containing the image which systemsettings5 may place under /tmp
       systemd.services.accounts-daemon.serviceConfig.PrivateTmp = false;
+      services.power-profiles-daemon.enable = mkDefault true;
+      services.system-config-printer.enable = mkIf config.services.printing.enable (mkDefault true);
       services.udisks2.enable = true;
       services.upower.enable = config.powerManagement.enable;
-      services.system-config-printer.enable = mkIf config.services.printing.enable (mkDefault true);
       services.xserver.libinput.enable = mkDefault true;
 
       # Extra UDEV rules used by Solid
@@ -457,27 +471,29 @@ in
       environment.systemPackages =
         with libsForQt5;
         with plasma5; with kdeGear; with kdeFrameworks;
-        [
-          ksystemstats
-          kinfocenter
-          kmenuedit
-          plasma-systemmonitor
-          spectacle
-          systemsettings
-
-          dolphin
-          dolphin-plugins
-          ffmpegthumbs
-          kdegraphics-thumbnailers
-          khelpcenter
-          kio-extras
-          print-manager
-
-          elisa
-          gwenview
-          okular
-        ]
-      ;
+        let
+          requiredPackages = [
+            ksystemstats
+            kinfocenter
+            kmenuedit
+            plasma-systemmonitor
+            spectacle
+            systemsettings
+
+            dolphin
+            dolphin-plugins
+            ffmpegthumbs
+            kdegraphics-thumbnailers
+            kio-extras
+          ];
+          optionalPackages = [
+            elisa
+            gwenview
+            okular
+            khelpcenter
+            print-manager
+          ];
+      in requiredPackages ++ utils.removePackagesByName optionalPackages cfg.excludePackages;
 
       systemd.user.services = {
         plasma-run-with-systemd = {
@@ -519,7 +535,7 @@ in
         with plasma5; with kdeApplications; with kdeFrameworks;
         [
           # Basic packages without which Plasma Mobile fails to work properly.
-          plasma-phone-components
+          plasma-mobile
           plasma-nano
           pkgs.maliit-framework
           pkgs.maliit-keyboard
@@ -573,7 +589,7 @@ in
         };
       };
 
-      services.xserver.displayManager.sessionPackages = [ pkgs.libsForQt5.plasma5.plasma-phone-components ];
+      services.xserver.displayManager.sessionPackages = [ pkgs.libsForQt5.plasma5.plasma-mobile ];
     })
   ];
 }
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index 3cf92f98c56fd..3a468b550627f 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -4,10 +4,9 @@ with lib;
 
 let
   cfg = config.services.xserver.desktopManager.xfce;
-in
 
+in
 {
-
   meta = {
     maintainers = teams.xfce.members;
   };
@@ -36,6 +35,12 @@ in
       [ "services" "xserver" "desktopManager" "xfce" "extraSessionCommands" ]
       [ "services" "xserver" "displayManager" "sessionCommands" ])
     (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "xfce" "screenLock" ] "")
+
+    # added 2022-06-26
+    # thunar has its own module
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce" "thunarPlugins" ]
+      [ "programs" "thunar" "plugins" ])
   ];
 
   options = {
@@ -46,15 +51,6 @@ in
         description = "Enable the Xfce desktop environment.";
       };
 
-      thunarPlugins = mkOption {
-        default = [];
-        type = types.listOf types.package;
-        example = literalExpression "[ pkgs.xfce.thunar-archive-plugin ]";
-        description = ''
-          A list of plugin that should be installed with Thunar.
-        '';
-      };
-
       noDesktop = mkOption {
         type = types.bool;
         default = false;
@@ -66,6 +62,12 @@ in
         default = true;
         description = "Enable the XFWM (default) window manager.";
       };
+
+      enableScreensaver = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Enable the XFCE screensaver.";
+      };
     };
   };
 
@@ -92,7 +94,6 @@ in
       exo
       garcon
       libxfce4ui
-      xfconf
 
       mousepad
       parole
@@ -104,8 +105,6 @@ in
       xfce4-settings
       xfce4-taskmanager
       xfce4-terminal
-
-      (thunar.override { thunarPlugins = cfg.thunarPlugins; })
     ] # TODO: NetworkManager doesn't belong here
       ++ optional config.networking.networkmanager.enable networkmanagerapplet
       ++ optional config.powerManagement.enable xfce4-power-manager
@@ -122,7 +121,10 @@ in
       ] ++ optionals (!cfg.noDesktop) [
         xfce4-panel
         xfdesktop
-      ];
+      ] ++ optional cfg.enableScreensaver xfce4-screensaver;
+
+    programs.xfconf.enable = true;
+    programs.thunar.enable = true;
 
     environment.pathsToLink = [
       "/share/xfce4"
@@ -164,9 +166,9 @@ in
 
     # Systemd services
     systemd.packages = with pkgs.xfce; [
-      (thunar.override { thunarPlugins = cfg.thunarPlugins; })
       xfce4-notifyd
     ];
 
+    security.pam.services.xfce4-screensaver.unixAuth = cfg.enableScreensaver;
   };
 }
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 92b3af8527f1b..a5db3dd5dd453 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -219,6 +219,7 @@ in
 
       session = mkOption {
         default = [];
+        type = types.listOf types.attrs;
         example = literalExpression
           ''
             [ { manage = "desktop";
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index b1dc6643be828..45e3d84afa4af 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -140,8 +140,13 @@ in
         environment = {
           GDM_X_SERVER_EXTRA_ARGS = toString
             (filter (arg: arg != "-terminate") cfg.xserverArgs);
-          # GDM is needed for gnome-login.session
-          XDG_DATA_DIRS = "${gdm}/share:${cfg.sessionData.desktops}/share";
+          XDG_DATA_DIRS = lib.makeSearchPath "share" [
+            gdm # for gnome-login.session
+            cfg.sessionData.desktops
+            pkgs.gnome.gnome-control-center # for accessibility icon
+            pkgs.gnome.adwaita-icon-theme
+            pkgs.hicolor-icon-theme # empty icon theme as a base
+          ];
         } // optionalAttrs (xSessionWrapper != null) {
           # Make GDM use this wrapper before running the session, which runs the
           # configured setupCommands. This relies on a patched GDM which supports
@@ -298,7 +303,7 @@ in
 
         session  required       pam_succeed_if.so audit quiet_success user = gdm
         session  required       pam_env.so conffile=/etc/pam/environment readenv=0
-        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       ${config.systemd.package}/lib/security/pam_systemd.so
         session  optional       pam_keyinit.so force revoke
         session  optional       pam_permit.so
       '';
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 84b75c83aeab4..302c8fe0d9171 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -267,6 +267,8 @@ in
     # Enable the accounts daemon to find lightdm's dbus interface
     environment.systemPackages = [ lightdm ];
 
+    security.polkit.enable = true;
+
     security.pam.services.lightdm.text = ''
         auth      substack      login
         account   include       login
@@ -285,7 +287,7 @@ in
 
         session  required       pam_succeed_if.so audit quiet_success user = lightdm
         session  required       pam_env.so conffile=/etc/pam/environment readenv=0
-        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       ${config.systemd.package}/lib/security/pam_systemd.so
         session  optional       pam_keyinit.so force revoke
         session  optional       pam_permit.so
     '';
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 529a086381f06..c44f24002e0b9 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -231,7 +231,7 @@ in
 
         session  required       pam_succeed_if.so audit quiet_success user = sddm
         session  required       pam_env.so conffile=/etc/pam/environment readenv=0
-        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       ${config.systemd.package}/lib/security/pam_systemd.so
         session  optional       pam_keyinit.so force revoke
         session  optional       pam_permit.so
       '';
diff --git a/nixos/modules/services/x11/display-managers/xpra.nix b/nixos/modules/services/x11/display-managers/xpra.nix
index c23e479140f09..1566e38da0839 100644
--- a/nixos/modules/services/x11/display-managers/xpra.nix
+++ b/nixos/modules/services/x11/display-managers/xpra.nix
@@ -26,6 +26,13 @@ in
         description = "Bind xpra to TCP";
       };
 
+      desktop = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "gnome-shell";
+        description = "Start a desktop environment instead of seamless mode";
+      };
+
       auth = mkOption {
         type = types.str;
         default = "pam";
@@ -222,7 +229,7 @@ in
     services.xserver.displayManager.job.execCmd = ''
       ${optionalString (cfg.pulseaudio)
         "export PULSE_COOKIE=/run/pulse/.config/pulse/cookie"}
-      exec ${pkgs.xpra}/bin/xpra start \
+      exec ${pkgs.xpra}/bin/xpra ${if cfg.desktop == null then "start" else "start-desktop --start=${cfg.desktop}"} \
         --daemon=off \
         --log-dir=/var/log \
         --log-file=xpra.log \
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index b40e20bcd3572..2eef71f71fcbf 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -51,6 +51,11 @@ in {
 
   imports = [
     (mkAliasOptionModule [ "services" "compton" ] [ "services" "picom" ])
+    (mkRemovedOptionModule [ "services" "picom" "refreshRate" ] ''
+      This option corresponds to `refresh-rate`, which has been unused
+      since picom v6 and was subsequently removed by upstream.
+      See https://github.com/yshui/picom/commit/bcbc410
+    '')
   ];
 
   options.services.picom = {
@@ -235,15 +240,6 @@ in {
       '';
     };
 
-    refreshRate = mkOption {
-      type = types.ints.unsigned;
-      default = 0;
-      example = 60;
-      description = ''
-       Screen refresh rate (0 = automatically detect).
-      '';
-    };
-
     settings = with types;
     let
       scalar = oneOf [ bool int float str ]
@@ -306,7 +302,6 @@ in {
       # other options
       backend          = cfg.backend;
       vsync            = cfg.vSync;
-      refresh-rate     = cfg.refreshRate;
     };
 
     systemd.user.services.picom = {
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index 835b41d4ada94..4d455fdf7b2d5 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -7,19 +7,26 @@ let
 in
 
 {
-  options = {
-    services.xserver.windowManager.qtile.enable = mkEnableOption "qtile";
+  options.services.xserver.windowManager.qtile = {
+    enable = mkEnableOption "qtile";
+
+    package = mkPackageOption pkgs "qtile" { };
   };
 
   config = mkIf cfg.enable {
     services.xserver.windowManager.session = [{
       name = "qtile";
       start = ''
-        ${pkgs.qtile}/bin/qtile start &
+        ${cfg.package}/bin/qtile start &
         waitPID=$!
       '';
     }];
 
-    environment.systemPackages = [ pkgs.qtile ];
+    environment.systemPackages = [
+      # pkgs.qtile is currently a buildenv of qtile and its dependencies.
+      # For userland commands, we want the underlying package so that
+      # packages such as python don't bleed into userland and overwrite intended behavior.
+      (cfg.package.unwrapped or cfg.package)
+    ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index 68f97c2f504b6..66d11131391f5 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -128,33 +128,34 @@ in {
             [ ( (mod4Mask,xK_r), compileRestart True)
             , ( (mod4Mask,xK_q), restart "xmonad" True ) ]
 
+          compileRestart resume = do
+            dirs  <- asks directories
+            whenX (recompile dirs True) $ do
+              when resume writeStateToFile
+              catchIO
+                  ( do
+                      args <- getArgs
+                      executeFile (cacheDir dirs </> compiledConfig) False args Nothing
+                  )
+
+          main = getDirectories >>= launch myConfig
+
           --------------------------------------------
-          {- version 0.17.0 -}
+          {- For versions before 0.17.0 use this instead -}
           --------------------------------------------
           -- compileRestart resume =
-          --   dirs <- io getDirectories
-          --   whenX (recompile dirs True) $
+          --   whenX (recompile True) $
           --     when resume writeStateToFile
           --       *> catchIO
           --         ( do
+          --             dir <- getXMonadDataDir
           --             args <- getArgs
-          --             executeFile (cacheDir dirs </> compiledConfig) False args Nothing
+          --             executeFile (dir </> compiledConfig) False args Nothing
           --         )
           --
-          -- main = getDirectories >>= launch myConfig
+          -- main = launch myConfig
           --------------------------------------------
 
-          compileRestart resume =
-            whenX (recompile True) $
-              when resume writeStateToFile
-                *> catchIO
-                  ( do
-                      dir <- getXMonadDataDir
-                      args <- getArgs
-                      executeFile (dir </> compiledConfig) False args Nothing
-                  )
-
-          main = launch myConfig
         '';
       };
 
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index ec6d86d59bdfb..d488e9b55d434 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, utils, pkgs, ... }:
 
 with lib;
 
@@ -181,6 +181,13 @@ in
         '';
       };
 
+      excludePackages = mkOption {
+        default = [];
+        example = literalExpression "[ pkgs.xterm ]";
+        type = types.listOf types.package;
+        description = "Which X11 packages to exclude from the default environment";
+      };
+
       exportConfiguration = mkOption {
         type = types.bool;
         default = false;
@@ -620,9 +627,6 @@ in
       in optional (driver != null) ({ inherit name; modules = []; driverName = name; display = true; } // driver));
 
     assertions = [
-      { assertion = config.security.polkit.enable;
-        message = "X11 requires Polkit to be enabled (‘security.polkit.enable = true’).";
-      }
       (let primaryHeads = filter (x: x.primary) cfg.xrandrHeads; in {
         assertion = length primaryHeads < 2;
         message = "Only one head is allowed to be primary in "
@@ -658,7 +662,7 @@ in
           ${cfgPath}.source = xorg.xf86inputevdev.out + "/share" + cfgPath;
         });
 
-    environment.systemPackages =
+    environment.systemPackages = utils.removePackagesByName
       [ xorg.xorgserver.out
         xorg.xrandr
         xorg.xrdb
@@ -674,7 +678,7 @@ in
         pkgs.xdg-utils
         xorg.xf86inputevdev.out # get evdev.4 man page
         pkgs.nixos-icons # needed for gnome and pantheon about dialog, nixos-manual and maybe more
-      ]
+      ] config.services.xserver.excludePackages
       ++ optional (elem "virtualbox" cfg.videoDrivers) xorg.xrefresh;
 
     environment.pathsToLink = [ "/share/X11" ];
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 1fe346114e439..3f0d976e70b88 100644..100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -1,42 +1,76 @@
 #! @perl@/bin/perl
 
+# Issue #166838 uncovered a situation in which a configuration not suitable
+# for the target architecture caused a cryptic error message instead of
+# a clean failure. Due to this mismatch, the perl interpreter in the shebang
+# line wasn't able to be executed, causing this script to be misinterpreted
+# as a shell script.
+#
+# Let's detect this situation to give a more meaningful error
+# message. The following two lines are carefully written to be both valid Perl
+# and Bash.
+printf "Perl script erroneously interpreted as shell script,\ndoes target platform match nixpkgs.crossSystem platform?\n" && exit 1
+    if 0;
+
 use strict;
 use warnings;
 use Config::IniFiles;
 use File::Path qw(make_path);
 use File::Basename;
-use File::Slurp;
+use File::Slurp qw(read_file write_file edit_file);
 use Net::DBus;
 use Sys::Syslog qw(:standard :macros);
-use Cwd 'abs_path';
+use Cwd qw(abs_path);
 
-my $out = "@out@";
+## no critic(ControlStructures::ProhibitDeepNests)
+## no critic(ErrorHandling::RequireCarping)
+## no critic(CodeLayout::ProhibitParensWithBuiltins)
+## no critic(Variables::ProhibitPunctuationVars, Variables::RequireLocalizedPunctuationVars)
+## no critic(InputOutput::RequireCheckedSyscalls, InputOutput::RequireBracedFileHandleWithPrint, InputOutput::RequireBriefOpen)
+## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals)
+## no critic(RegularExpressions::ProhibitEscapedMetacharacters)
 
-my $curSystemd = abs_path("/run/current-system/sw/bin");
+# System closure path to switch to
+my $out = "@out@";
+# Path to the directory containing systemd tools of the old system
+my $cur_systemd = abs_path("/run/current-system/sw/bin");
+# Path to the systemd store path of the new system
+my $new_systemd = "@systemd@";
 
 # To be robust against interruption, record what units need to be started etc.
-my $startListFile = "/run/nixos/start-list";
-my $restartListFile = "/run/nixos/restart-list";
-my $reloadListFile = "/run/nixos/reload-list";
+# We read these files again every time this script starts to make sure we continue
+# where the old (interrupted) script left off.
+my $start_list_file = "/run/nixos/start-list";
+my $restart_list_file = "/run/nixos/restart-list";
+my $reload_list_file = "/run/nixos/reload-list";
 
 # Parse restart/reload requests by the activation script.
-# Activation scripts may write newline-separated units to this
+# Activation scripts may write newline-separated units to the restart
 # file and switch-to-configuration will handle them. While
 # `stopIfChanged = true` is ignored, switch-to-configuration will
 # handle `restartIfChanged = false` and `reloadIfChanged = true`.
-my $restartByActivationFile = "/run/nixos/activation-restart-list";
-my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list";
-
-make_path("/run/nixos", { mode => oct(755) });
-
-my $action = shift @ARGV;
+# This is the same as specifying a restart trigger in the NixOS module.
+#
+# The reload file asks the script to reload a unit. This is the same as
+# specifying a reload trigger in the NixOS module and can be ignored if
+# the unit is restarted in this activation.
+my $restart_by_activation_file = "/run/nixos/activation-restart-list";
+my $reload_by_activation_file = "/run/nixos/activation-reload-list";
+my $dry_restart_by_activation_file = "/run/nixos/dry-activation-restart-list";
+my $dry_reload_by_activation_file = "/run/nixos/dry-activation-reload-list";
+
+# The action that is to be performed (like switch, boot, test, dry-activate)
+# Also exposed via environment variable from now on
+my $action = shift(@ARGV);
+$ENV{NIXOS_ACTION} = $action;
 
+# Expose the locale archive as an environment variable for systemctl and the activation script
 if ("@localeArchive@" ne "") {
     $ENV{LOCALE_ARCHIVE} = "@localeArchive@";
 }
 
-if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) {
-    print STDERR <<EOF;
+if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) {
+    print STDERR <<"EOF";
 Usage: $0 [switch|boot|test]
 
 switch:       make the configuration the boot default and activate now
@@ -44,67 +78,102 @@ boot:         make the configuration the boot default
 test:         activate the configuration, but don\'t make it the boot default
 dry-activate: show what would be done if this configuration were activated
 EOF
-    exit 1;
+    exit(1);
 }
 
-$ENV{NIXOS_ACTION} = $action;
-
 # This is a NixOS installation if it has /etc/NIXOS or a proper
 # /etc/os-release.
-die "This is not a NixOS installation!\n" unless
-    -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID=nixos/s;
+if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?nixos"?/msx) {
+    die("This is not a NixOS installation!\n");
+}
 
+make_path("/run/nixos", { mode => oct(755) });
 openlog("nixos", "", LOG_USER);
 
 # Install or update the bootloader.
 if ($action eq "switch" || $action eq "boot") {
-    system("@installBootLoader@ $out") == 0 or exit 1;
+    chomp(my $install_boot_loader = <<'EOFBOOTLOADER');
+@installBootLoader@
+EOFBOOTLOADER
+    system("$install_boot_loader $out") == 0 or exit 1;
 }
 
 # Just in case the new configuration hangs the system, do a sync now.
-system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1";
+if (($ENV{"NIXOS_NO_SYNC"} // "") ne "1") {
+    system("@coreutils@/bin/sync", "-f", "/nix/store");
+}
 
-exit 0 if $action eq "boot";
+if ($action eq "boot") {
+    exit(0);
+}
 
 # Check if we can activate the new configuration.
-my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // "";
-my $newVersion = read_file("$out/init-interface-version");
+my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // "";
+my $new_init_interface_version = read_file("$out/init-interface-version");
 
-if ($newVersion ne $oldVersion) {
-    print STDERR <<EOF;
+if ($new_init_interface_version ne $cur_init_interface_version) {
+    print STDERR <<'EOF';
 Warning: the new NixOS configuration has an ‘init’ that is
 incompatible with the current configuration.  The new configuration
-won\'t take effect until you reboot the system.
+won't take effect until you reboot the system.
 EOF
-    exit 100;
+    exit(100);
 }
 
 # Ignore SIGHUP so that we're not killed if we're running on (say)
 # virtual console 1 and we restart the "tty1" unit.
 $SIG{PIPE} = "IGNORE";
 
-sub getActiveUnits {
+# Asks the currently running systemd instance via dbus which units are active.
+# Returns a hash where the key is the name of each unit and the value a hash
+# of load, state, substate.
+sub get_active_units {
     my $mgr = Net::DBus->system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1");
     my $units = $mgr->ListUnitsByPatterns([], []);
     my $res = {};
-    for my $item (@$units) {
+    for my $item (@{$units}) {
         my ($id, $description, $load_state, $active_state, $sub_state,
-            $following, $unit_path, $job_id, $job_type, $job_path) = @$item;
-        next unless $following eq '';
-        next if $job_id == 0 and $active_state eq 'inactive';
+            $following, $unit_path, $job_id, $job_type, $job_path) = @{$item};
+        if ($following ne "") {
+            next;
+        }
+        if ($job_id == 0 and $active_state eq "inactive") {
+            next;
+        }
         $res->{$id} = { load => $load_state, state => $active_state, substate => $sub_state };
     }
     return $res;
 }
 
-sub parseFstab {
+# Asks the currently running systemd instance whether a unit is currently active.
+# Takes the name of the unit as an argument and returns a bool whether the unit is active or not.
+sub unit_is_active {
+    my ($unit_name) = @_;
+
+    my $mgr = Net::DBus->system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1");
+    my $units = $mgr->ListUnitsByNames([$unit_name]);
+    if (scalar(@{$units}) == 0) {
+        return 0;
+    }
+    my $active_state = $units->[0]->[3];
+    return $active_state eq "active" || $active_state eq "activating";
+}
+
+# Parse a fstab file, given its path.
+# Returns a tuple of filesystems and swaps.
+#
+# Filesystems is a hash of mountpoint and { device, fsType, options }
+# Swaps is a hash of device and { options }
+sub parse_fstab {
     my ($filename) = @_;
     my ($fss, $swaps);
-    foreach my $line (read_file($filename, err_mode => 'quiet')) {
-        chomp $line;
-        $line =~ s/^\s*#.*//;
-        next if $line =~ /^\s*$/;
-        my @xs = split / /, $line;
+    foreach my $line (read_file($filename, err_mode => "quiet")) {
+        chomp($line);
+        $line =~ s/^\s*\#.*//msx;
+        if ($line =~ /^\s*$/msx) {
+            next;
+        }
+        my @xs = split(/\s+/msx, $line);
         if ($xs[2] eq "swap") {
             $swaps->{$xs[0]} = { options => $xs[3] // "" };
         } else {
@@ -123,32 +192,35 @@ sub parseFstab {
 #
 # Instead of returning the hash, this subroutine takes a hashref to return the data in. This
 # allows calling the subroutine multiple times with the same hash to parse override files.
-sub parseSystemdIni {
-    my ($unitContents, $path) = @_;
+sub parse_systemd_ini {
+    my ($unit_contents, $path) = @_;
     # Tie the ini file to a hash for easier access
-    my %fileContents;
-    tie %fileContents, "Config::IniFiles", (-file => $path, -allowempty => 1, -allowcontinue => 1);
+    tie(my %file_contents, "Config::IniFiles", (-file => $path, -allowempty => 1, -allowcontinue => 1)); ## no critic(Miscellanea::ProhibitTies)
 
     # Copy over all sections
-    foreach my $sectionName (keys %fileContents) {
+    foreach my $section_name (keys(%file_contents)) {
+        if ($section_name eq "Install") {
+            # Skip the [Install] section because it has no relevant keys for us
+            next;
+        }
         # Copy over all keys
-        foreach my $iniKey (keys %{$fileContents{$sectionName}}) {
+        foreach my $ini_key (keys(%{$file_contents{$section_name}})) {
             # Ensure the value is an array so it's easier to work with
-            my $iniValue = $fileContents{$sectionName}{$iniKey};
-            my @iniValues;
-            if (ref($iniValue) eq "ARRAY") {
-                @iniValues = @{$iniValue};
+            my $ini_value = $file_contents{$section_name}{$ini_key};
+            my @ini_values;
+            if (ref($ini_value) eq "ARRAY") {
+                @ini_values = @{$ini_value};
             } else {
-                @iniValues = $iniValue;
+                @ini_values = $ini_value;
             }
             # Go over all values
-            for my $iniValue (@iniValues) {
+            for my $ini_value (@ini_values) {
                 # If a value is empty, it's an override that tells us to clean the value
-                if ($iniValue eq "") {
-                    delete $unitContents->{$sectionName}->{$iniKey};
+                if ($ini_value eq "") {
+                    delete $unit_contents->{$section_name}->{$ini_key};
                     next;
                 }
-                push(@{$unitContents->{$sectionName}->{$iniKey}}, $iniValue);
+                push(@{$unit_contents->{$section_name}->{$ini_key}}, $ini_value);
             }
         }
     }
@@ -157,102 +229,251 @@ sub parseSystemdIni {
 
 # This subroutine takes the path to a systemd configuration file (like a unit configuration),
 # parses it, and returns a hash that contains the contents. The contents of this hash are
-# explained in the `parseSystemdIni` subroutine. Neither the sections nor the keys inside
+# explained in the `parse_systemd_ini` subroutine. Neither the sections nor the keys inside
 # the sections are consistently sorted.
 #
 # If a directory with the same basename ending in .d exists next to the unit file, it will be
 # assumed to contain override files which will be parsed as well and handled properly.
-sub parseUnit {
-    my ($unitPath) = @_;
+sub parse_unit {
+    my ($unit_path) = @_;
 
     # Parse the main unit and all overrides
-    my %unitData;
-    parseSystemdIni(\%unitData, $_) for glob("${unitPath}{,.d/*.conf}");
-    return %unitData;
+    my %unit_data;
+    # Replace \ with \\ so glob() still works with units that have a \ in them
+    # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
+    $unit_path =~ s/\\/\\\\/gmsx;
+    foreach (glob("${unit_path}{,.d/*.conf}")) {
+        parse_systemd_ini(\%unit_data, "$_")
+    }
+    return %unit_data;
 }
 
 # Checks whether a specified boolean in a systemd unit is true
 # or false, with a default that is applied when the value is not set.
-sub parseSystemdBool {
-    my ($unitConfig, $sectionName, $boolName, $default) = @_;
+sub parse_systemd_bool {
+    my ($unit_config, $section_name, $bool_name, $default) = @_;
 
-    my @values = @{$unitConfig->{$sectionName}{$boolName} // []};
+    my @values = @{$unit_config->{$section_name}{$bool_name} // []};
     # Return default if value is not set
-    if (scalar @values lt 1 || not defined $values[-1]) {
+    if ((scalar(@values) < 1) || (not defined($values[-1]))) {
         return $default;
     }
     # If value is defined multiple times, use the last definition
-    my $last = $values[-1];
+    my $last_value = $values[-1];
     # These are valid values as of systemd.syntax(7)
-    return $last eq "1" || $last eq "yes" || $last eq "true" || $last eq "on";
+    return $last_value eq "1" || $last_value eq "yes" || $last_value eq "true" || $last_value eq "on";
 }
 
-sub recordUnit {
+# Writes a unit name into a given file to be more resilient against
+# crashes of the script. Does nothing when the action is dry-activate.
+sub record_unit {
     my ($fn, $unit) = @_;
-    write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate";
+    if ($action ne "dry-activate") {
+        write_file($fn, { append => 1 }, "$unit\n");
+    }
+    return;
 }
 
-# As a fingerprint for determining whether a unit has changed, we use
-# its absolute path. If it has an override file, we append *its*
-# absolute path as well.
-sub fingerprintUnit {
-    my ($s) = @_;
-    return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : "");
+# The opposite of record_unit, removes a unit name from a file
+sub unrecord_unit {
+    my ($fn, $unit) = @_;
+    if ($action ne "dry-activate") {
+        edit_file(sub { s/^$unit\n//msx }, $fn);
+    }
+    return;
+}
+
+# Compare the contents of two unit files and return whether the unit
+# needs to be restarted or reloaded. If the units differ, the service
+# is restarted unless the only difference is `X-Reload-Triggers` in the
+# `Unit` section. If this is the only modification, the unit is reloaded
+# instead of restarted.
+# Returns:
+# - 0 if the units are equal
+# - 1 if the units are different and a restart action is required
+# - 2 if the units are different and a reload action is required
+sub compare_units { ## no critic(Subroutines::ProhibitExcessComplexity)
+    my ($cur_unit, $new_unit) = @_;
+    my $ret = 0;
+    # Keys to ignore in the [Unit] section
+    my %unit_section_ignores = map { $_ => 1 } qw(
+        X-Reload-Triggers
+        Description Documentation
+        OnFailure OnSuccess OnFailureJobMode
+        IgnoreOnIsolate StopWhenUnneeded
+        RefuseManualStart RefuseManualStop
+        AllowIsolate CollectMode
+        SourcePath
+    );
+
+    my $comp_array = sub {
+      my ($a, $b) = @_;
+      return join("\0", @{$a}) eq join("\0", @{$b});
+    };
+
+    # Comparison hash for the sections
+    my %section_cmp = map { $_ => 1 } keys(%{$new_unit});
+    # Iterate over the sections
+    foreach my $section_name (keys(%{$cur_unit})) {
+        # Missing section in the new unit?
+        if (not exists($section_cmp{$section_name})) {
+            # If the [Unit] section was removed, make sure that only keys
+            # were in it that are ignored
+            if ($section_name eq "Unit") {
+                foreach my $ini_key (keys(%{$cur_unit->{"Unit"}})) {
+                    if (not defined($unit_section_ignores{$ini_key})) {
+                        return 1;
+                    }
+                }
+                next; # check the next section
+            } else {
+                return 1;
+            }
+            if ($section_name eq "Unit" and %{$cur_unit->{"Unit"}} == 1 and defined(%{$cur_unit->{"Unit"}}{"X-Reload-Triggers"})) {
+                # If a new [Unit] section was removed that only contained X-Reload-Triggers,
+                # do nothing.
+                next;
+            } else {
+                return 1;
+            }
+        }
+        delete $section_cmp{$section_name};
+        # Comparison hash for the section contents
+        my %ini_cmp = map { $_ => 1 } keys(%{$new_unit->{$section_name}});
+        # Iterate over the keys of the section
+        foreach my $ini_key (keys(%{$cur_unit->{$section_name}})) {
+            delete $ini_cmp{$ini_key};
+            my @cur_value = @{$cur_unit->{$section_name}{$ini_key}};
+            # If the key is missing in the new unit, they are different...
+            if (not $new_unit->{$section_name}{$ini_key}) {
+                # ... unless the key that is now missing is one of the ignored keys
+                if ($section_name eq "Unit" and defined($unit_section_ignores{$ini_key})) {
+                    next;
+                }
+                return 1;
+            }
+            my @new_value = @{$new_unit->{$section_name}{$ini_key}};
+            # If the contents are different, the units are different
+            if (not $comp_array->(\@cur_value, \@new_value)) {
+                # Check if only the reload triggers changed or one of the ignored keys
+                if ($section_name eq "Unit") {
+                    if ($ini_key eq "X-Reload-Triggers") {
+                        $ret = 2;
+                        next;
+                    } elsif (defined($unit_section_ignores{$ini_key})) {
+                        next;
+                    }
+                }
+                return 1;
+            }
+        }
+        # A key was introduced that was missing in the previous unit
+        if (%ini_cmp) {
+            if ($section_name eq "Unit") {
+                foreach my $ini_key (keys(%ini_cmp)) {
+                    if ($ini_key eq "X-Reload-Triggers") {
+                        $ret = 2;
+                    } elsif (defined($unit_section_ignores{$ini_key})) {
+                        next;
+                    } else {
+                        return 1;
+                    }
+                }
+            } else {
+                return 1;
+            }
+        };
+    }
+    # A section was introduced that was missing in the previous unit
+    if (%section_cmp) {
+        if (%section_cmp == 1 and defined($section_cmp{"Unit"})) {
+            foreach my $ini_key (keys(%{$new_unit->{"Unit"}})) {
+                if (not defined($unit_section_ignores{$ini_key})) {
+                    return 1;
+                } elsif ($ini_key eq "X-Reload-Triggers") {
+                    $ret = 2;
+                }
+            }
+        } else {
+            return 1;
+        }
+    }
+
+    return $ret;
 }
 
-sub handleModifiedUnit {
-    my ($unit, $baseName, $newUnitFile, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
+# Called when a unit exists in both the old systemd and the new system and the units
+# differ. This figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
+sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutines::ProhibitExcessComplexity)
+    my ($unit, $base_name, $new_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;
 
-    if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
+    if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/msx || $unit =~ /\.slice$/msx) {
         # Do nothing.  These cannot be restarted directly.
 
         # Slices and Paths don't have to be restarted since
         # properties (resource limits and inotify watches)
         # seem to get applied on daemon-reload.
-    } elsif ($unit =~ /\.mount$/) {
+    } elsif ($unit =~ /\.mount$/msx) {
         # Reload the changed mount unit to force a remount.
-        $unitsToReload->{$unit} = 1;
-        recordUnit($reloadListFile, $unit);
-    } elsif ($unit =~ /\.socket$/) {
+        # FIXME: only reload when Options= changed, restart otherwise
+        $units_to_reload->{$unit} = 1;
+        record_unit($reload_list_file, $unit);
+    } elsif ($unit =~ /\.socket$/msx) {
         # FIXME: do something?
         # Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
         # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
         # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
     } else {
-        my %unitInfo = parseUnit($newUnitFile);
-        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0)) {
-            $unitsToReload->{$unit} = 1;
-            recordUnit($reloadListFile, $unit);
+        my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file);
+        if (parse_systemd_bool(\%new_unit_info, "Service", "X-ReloadIfChanged", 0) and not $units_to_restart->{$unit} and not $units_to_stop->{$unit}) {
+            $units_to_reload->{$unit} = 1;
+            record_unit($reload_list_file, $unit);
         }
-        elsif (!parseSystemdBool(\%unitInfo, "Service", "X-RestartIfChanged", 1) || parseSystemdBool(\%unitInfo, "Unit", "RefuseManualStop", 0) || parseSystemdBool(\%unitInfo, "Unit", "X-OnlyManualStart", 0)) {
-            $unitsToSkip->{$unit} = 1;
+        elsif (!parse_systemd_bool(\%new_unit_info, "Service", "X-RestartIfChanged", 1) || parse_systemd_bool(\%new_unit_info, "Unit", "RefuseManualStop", 0) || parse_systemd_bool(\%new_unit_info, "Unit", "X-OnlyManualStart", 0)) {
+            $units_to_skip->{$unit} = 1;
         } else {
             # It doesn't make sense to stop and start non-services because
             # they can't have ExecStop=
-            if (!parseSystemdBool(\%unitInfo, "Service", "X-StopIfChanged", 1) || $unit !~ /\.service$/) {
+            if (!parse_systemd_bool(\%new_unit_info, "Service", "X-StopIfChanged", 1) || $unit !~ /\.service$/msx) {
                 # This unit should be restarted instead of
                 # stopped and started.
-                $unitsToRestart->{$unit} = 1;
-                recordUnit($restartListFile, $unit);
+                $units_to_restart->{$unit} = 1;
+                record_unit($restart_list_file, $unit);
+                # Remove from units to reload so we don't restart and reload
+                if ($units_to_reload->{$unit}) {
+                    delete $units_to_reload->{$unit};
+                    unrecord_unit($reload_list_file, $unit);
+                }
             } else {
                 # If this unit is socket-activated, then stop the
                 # socket unit(s) as well, and restart the
                 # socket(s) instead of the service.
-                my $socketActivated = 0;
-                if ($unit =~ /\.service$/) {
-                    my @sockets = split(/ /, join(" ", @{$unitInfo{Service}{Sockets} // []}));
-                    if (scalar @sockets == 0) {
-                        @sockets = ("$baseName.socket");
+                my $socket_activated = 0;
+                if ($unit =~ /\.service$/msx) {
+                    my @sockets = split(/\s+/msx, join(" ", @{$new_unit_info{Service}{Sockets} // []}));
+                    if (scalar(@sockets) == 0) {
+                        @sockets = ("$base_name.socket");
                     }
                     foreach my $socket (@sockets) {
-                        if (defined $activePrev->{$socket}) {
-                            $unitsToStop->{$socket} = 1;
+                        if (defined($active_cur->{$socket})) {
+                            # We can now be sure this is a socket-activate unit
+
+                            $units_to_stop->{$socket} = 1;
                             # Only restart sockets that actually
                             # exist in new configuration:
                             if (-e "$out/etc/systemd/system/$socket") {
-                                $unitsToStart->{$socket} = 1;
-                                recordUnit($startListFile, $socket);
-                                $socketActivated = 1;
+                                $units_to_start->{$socket} = 1;
+                                if ($units_to_start eq $units_to_restart) {
+                                    record_unit($restart_list_file, $socket);
+                                } else {
+                                    record_unit($start_list_file, $socket);
+                                }
+                                $socket_activated = 1;
+                            }
+                            # Remove from units to reload so we don't restart and reload
+                            if ($units_to_reload->{$unit}) {
+                                delete $units_to_reload->{$unit};
+                                unrecord_unit($reload_list_file, $unit);
                             }
                         }
                     }
@@ -262,56 +483,68 @@ sub handleModifiedUnit {
                 # that this unit needs to be started below.
                 # We write this to a file to ensure that the
                 # service gets restarted if we're interrupted.
-                if (!$socketActivated) {
-                    $unitsToStart->{$unit} = 1;
-                    recordUnit($startListFile, $unit);
+                if (!$socket_activated) {
+                    $units_to_start->{$unit} = 1;
+                    if ($units_to_start eq $units_to_restart) {
+                        record_unit($restart_list_file, $unit);
+                    } else {
+                        record_unit($start_list_file, $unit);
+                    }
                 }
 
-                $unitsToStop->{$unit} = 1;
+                $units_to_stop->{$unit} = 1;
+                # Remove from units to reload so we don't restart and reload
+                if ($units_to_reload->{$unit}) {
+                    delete $units_to_reload->{$unit};
+                    unrecord_unit($reload_list_file, $unit);
+                }
             }
         }
     }
+    return;
 }
 
 # Figure out what units need to be stopped, started, restarted or reloaded.
-my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload);
+my (%units_to_stop, %units_to_skip, %units_to_start, %units_to_restart, %units_to_reload);
 
-my %unitsToFilter; # units not shown
+my %units_to_filter; # units not shown
 
-$unitsToStart{$_} = 1 foreach
-    split('\n', read_file($startListFile, err_mode => 'quiet') // "");
+%units_to_start = map { $_ => 1 }
+    split(/\n/msx, read_file($start_list_file, err_mode => "quiet") // "");
 
-$unitsToRestart{$_} = 1 foreach
-    split('\n', read_file($restartListFile, err_mode => 'quiet') // "");
+%units_to_restart = map { $_ => 1 }
+    split(/\n/msx, read_file($restart_list_file, err_mode => "quiet") // "");
 
-$unitsToReload{$_} = 1 foreach
-    split('\n', read_file($reloadListFile, err_mode => 'quiet') // "");
+%units_to_reload = map { $_ => 1 }
+    split(/\n/msx, read_file($reload_list_file, err_mode => "quiet") // "");
 
-my $activePrev = getActiveUnits;
-while (my ($unit, $state) = each %{$activePrev}) {
-    my $baseUnit = $unit;
+my $active_cur = get_active_units();
+while (my ($unit, $state) = each(%{$active_cur})) {
+    my $base_unit = $unit;
 
-    my $prevUnitFile = "/etc/systemd/system/$baseUnit";
-    my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    my $cur_unit_file = "/etc/systemd/system/$base_unit";
+    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
 
     # Detect template instances.
-    if (!-e $prevUnitFile && !-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
-      $baseUnit = "$1\@.$2";
-      $prevUnitFile = "/etc/systemd/system/$baseUnit";
-      $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
+      $base_unit = "$1\@.$2";
+      $cur_unit_file = "/etc/systemd/system/$base_unit";
+      $new_unit_file = "$out/etc/systemd/system/$base_unit";
     }
 
-    my $baseName = $baseUnit;
-    $baseName =~ s/\.[a-z]*$//;
+    my $base_name = $base_unit;
+    $base_name =~ s/\.[[:lower:]]*$//msx;
 
-    if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) {
-        if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") {
-            my %unitInfo = parseUnit($prevUnitFile);
-            $unitsToStop{$unit} = 1 if parseSystemdBool(\%unitInfo, "Unit", "X-StopOnRemoval", 1);
+    if (-e $cur_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
+        if (! -e $new_unit_file || abs_path($new_unit_file) eq "/dev/null") {
+            my %cur_unit_info = parse_unit($cur_unit_file);
+            if (parse_systemd_bool(\%cur_unit_info, "Unit", "X-StopOnRemoval", 1)) {
+                $units_to_stop{$unit} = 1;
+            }
         }
 
-        elsif ($unit =~ /\.target$/) {
-            my %unitInfo = parseUnit($newUnitFile);
+        elsif ($unit =~ /\.target$/msx) {
+            my %new_unit_info = parse_unit($new_unit_file);
 
             # Cause all active target units to be restarted below.
             # This should start most changed units we stop here as
@@ -320,11 +553,11 @@ while (my ($unit, $state) = each %{$activePrev}) {
             # active after the system has resumed, which probably
             # should not be the case.  Just ignore it.
             if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") {
-                unless (parseSystemdBool(\%unitInfo, "Unit", "RefuseManualStart", 0) || parseSystemdBool(\%unitInfo, "Unit", "X-OnlyManualStart", 0)) {
-                    $unitsToStart{$unit} = 1;
-                    recordUnit($startListFile, $unit);
+                if (!(parse_systemd_bool(\%new_unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%new_unit_info, "Unit", "X-OnlyManualStart", 0))) {
+                    $units_to_start{$unit} = 1;
+                    record_unit($start_list_file, $unit);
                     # Don't spam the user with target units that always get started.
-                    $unitsToFilter{$unit} = 1;
+                    $units_to_filter{$unit} = 1;
                 }
             }
 
@@ -339,163 +572,188 @@ while (my ($unit, $state) = each %{$activePrev}) {
             # Stopping a target generally has no effect on other units
             # (unless there is a PartOf dependency), so this is just a
             # bookkeeping thing to get systemd to do the right thing.
-            if (parseSystemdBool(\%unitInfo, "Unit", "X-StopOnReconfiguration", 0)) {
-                $unitsToStop{$unit} = 1;
+            if (parse_systemd_bool(\%new_unit_info, "Unit", "X-StopOnReconfiguration", 0)) {
+                $units_to_stop{$unit} = 1;
             }
         }
 
-        elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) {
-            handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        else {
+            my %cur_unit_info = parse_unit($cur_unit_file);
+            my %new_unit_info = parse_unit($new_unit_file);
+            my $diff = compare_units(\%cur_unit_info, \%new_unit_info);
+            if ($diff == 1) {
+                handle_modified_unit($unit, $base_name, $new_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
+            } elsif ($diff == 2 and not $units_to_restart{$unit}) {
+                $units_to_reload{$unit} = 1;
+                record_unit($reload_list_file, $unit);
+            }
         }
     }
 }
 
-sub pathToUnitName {
+# Converts a path to the name of a systemd mount unit that would be responsible
+# for mounting this path.
+sub path_to_unit_name {
     my ($path) = @_;
     # Use current version of systemctl binary before daemon is reexeced.
-    open my $cmd, "-|", "$curSystemd/systemd-escape", "--suffix=mount", "-p", $path
+    open(my $cmd, "-|", "$cur_systemd/systemd-escape", "--suffix=mount", "-p", $path)
         or die "Unable to escape $path!\n";
-    my $escaped = join "", <$cmd>;
-    chomp $escaped;
-    close $cmd or die;
+    my $escaped = do { local $/ = undef; <$cmd> };
+    chomp($escaped);
+    close($cmd) or die("Unable to close systemd-escape pipe");
     return $escaped;
 }
 
-sub unique {
-    my %seen;
-    my @res;
-    foreach my $name (@_) {
-        next if $seen{$name};
-        $seen{$name} = 1;
-        push @res, $name;
-    }
-    return @res;
-}
-
 # Compare the previous and new fstab to figure out which filesystems
 # need a remount or need to be unmounted.  New filesystems are mounted
 # automatically by starting local-fs.target.  FIXME: might be nicer if
 # we generated units for all mounts; then we could unify this with the
 # unit checking code above.
-my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab";
-my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab";
-foreach my $mountPoint (keys %$prevFss) {
-    my $prev = $prevFss->{$mountPoint};
-    my $new = $newFss->{$mountPoint};
-    my $unit = pathToUnitName($mountPoint);
-    if (!defined $new) {
+my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab");
+my ($new_fss, $new_swaps) = parse_fstab("$out/etc/fstab");
+foreach my $mount_point (keys(%{$cur_fss})) {
+    my $cur = $cur_fss->{$mount_point};
+    my $new = $new_fss->{$mount_point};
+    my $unit = path_to_unit_name($mount_point);
+    if (!defined($new)) {
         # Filesystem entry disappeared, so unmount it.
-        $unitsToStop{$unit} = 1;
-    } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) {
+        $units_to_stop{$unit} = 1;
+    } elsif ($cur->{fsType} ne $new->{fsType} || $cur->{device} ne $new->{device}) {
         # Filesystem type or device changed, so unmount and mount it.
-        $unitsToStop{$unit} = 1;
-        $unitsToStart{$unit} = 1;
-        recordUnit($startListFile, $unit);
-    } elsif ($prev->{options} ne $new->{options}) {
+        $units_to_stop{$unit} = 1;
+        $units_to_start{$unit} = 1;
+        record_unit($start_list_file, $unit);
+    } elsif ($cur->{options} ne $new->{options}) {
         # Mount options changes, so remount it.
-        $unitsToReload{$unit} = 1;
-        recordUnit($reloadListFile, $unit);
+        $units_to_reload{$unit} = 1;
+        record_unit($reload_list_file, $unit);
     }
 }
 
 # Also handles swap devices.
-foreach my $device (keys %$prevSwaps) {
-    my $prev = $prevSwaps->{$device};
-    my $new = $newSwaps->{$device};
-    if (!defined $new) {
+foreach my $device (keys(%{$cur_swaps})) {
+    my $cur = $cur_swaps->{$device};
+    my $new = $new_swaps->{$device};
+    if (!defined($new)) {
         # Swap entry disappeared, so turn it off.  Can't use
         # "systemctl stop" here because systemd has lots of alias
         # units that prevent a stop from actually calling
         # "swapoff".
-        print STDERR "stopping swap device: $device\n";
-        system("@utillinux@/sbin/swapoff", $device);
+        if ($action ne "dry-activate") {
+            print STDERR "would stop swap device: $device\n";
+        } else {
+            print STDERR "stopping swap device: $device\n";
+            system("@utillinux@/sbin/swapoff", $device);
+        }
     }
     # FIXME: update swap options (i.e. its priority).
 }
 
 
 # Should we have systemd re-exec itself?
-my $prevSystemd = abs_path("/proc/1/exe") // "/unknown";
-my $prevSystemdSystemConfig = abs_path("/etc/systemd/system.conf") // "/unknown";
-my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die;
-my $newSystemdSystemConfig = abs_path("$out/etc/systemd/system.conf") // "/unknown";
-
-my $restartSystemd = $prevSystemd ne $newSystemd;
-if ($prevSystemdSystemConfig ne $newSystemdSystemConfig) {
-    $restartSystemd = 1;
+my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown";
+my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown";
+my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die;
+my $new_systemd_system_config = abs_path("$out/etc/systemd/system.conf") // "/unknown";
+
+my $restart_systemd = $cur_pid1_path ne $new_pid1_path;
+if ($cur_systemd_system_config ne $new_systemd_system_config) {
+    $restart_systemd = 1;
 }
 
-
-sub filterUnits {
+# Takes an array of unit names and returns an array with the same elements,
+# except all units that are also in the global variable `unitsToFilter`.
+sub filter_units {
     my ($units) = @_;
     my @res;
-    foreach my $unit (sort(keys %{$units})) {
-        push @res, $unit if !defined $unitsToFilter{$unit};
+    foreach my $unit (sort(keys(%{$units}))) {
+        if (!defined($units_to_filter{$unit})) {
+            push(@res, $unit);
+        }
     }
     return @res;
 }
 
-my @unitsToStopFiltered = filterUnits(\%unitsToStop);
+my @units_to_stop_filtered = filter_units(\%units_to_stop);
 
 
 # Show dry-run actions.
 if ($action eq "dry-activate") {
-    print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n"
-        if scalar @unitsToStopFiltered > 0;
-    print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n"
-        if scalar(keys %unitsToSkip) > 0;
+    if (scalar(@units_to_stop_filtered) > 0) {
+        print STDERR "would stop the following units: ", join(", ", @units_to_stop_filtered), "\n";
+    }
+    if (scalar(keys(%units_to_skip)) > 0) {
+        print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys(%units_to_skip))), "\n";
+    }
 
     print STDERR "would activate the configuration...\n";
     system("$out/dry-activate", "$out");
 
     # Handle the activation script requesting the restart or reload of a unit.
-    foreach (split('\n', read_file($dryRestartByActivationFile, err_mode => 'quiet') // "")) {
+    foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
         my $unit = $_;
-        my $baseUnit = $unit;
-        my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+        my $base_unit = $unit;
+        my $new_unit_file = "$out/etc/systemd/system/$base_unit";
 
         # Detect template instances.
-        if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
-          $baseUnit = "$1\@.$2";
-          $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+        if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
+          $base_unit = "$1\@.$2";
+          $new_unit_file = "$out/etc/systemd/system/$base_unit";
         }
 
-        my $baseName = $baseUnit;
-        $baseName =~ s/\.[a-z]*$//;
+        my $base_name = $base_unit;
+        $base_name =~ s/\.[[:lower:]]*$//msx;
 
         # Start units if they were not active previously
-        if (not defined $activePrev->{$unit}) {
-            $unitsToStart{$unit} = 1;
+        if (not defined($active_cur->{$unit})) {
+            $units_to_start{$unit} = 1;
             next;
         }
 
-        handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
+    }
+    unlink($dry_restart_by_activation_file);
+
+    foreach (split(/\n/msx, read_file($dry_reload_by_activation_file, err_mode => "quiet") // "")) {
+        my $unit = $_;
+
+        if (defined($active_cur->{$unit}) and not $units_to_restart{$unit} and not $units_to_stop{$unit}) {
+            $units_to_reload{$unit} = 1;
+            record_unit($reload_list_file, $unit);
+        }
     }
-    unlink($dryRestartByActivationFile);
+    unlink($dry_reload_by_activation_file);
 
-    print STDERR "would restart systemd\n" if $restartSystemd;
-    print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
-        if scalar(keys %unitsToReload) > 0;
-    print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"
-        if scalar(keys %unitsToRestart) > 0;
-    my @unitsToStartFiltered = filterUnits(\%unitsToStart);
-    print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n"
-        if scalar @unitsToStartFiltered;
+    if ($restart_systemd) {
+        print STDERR "would restart systemd\n";
+    }
+    if (scalar(keys(%units_to_reload)) > 0) {
+        print STDERR "would reload the following units: ", join(", ", sort(keys(%units_to_reload))), "\n";
+    }
+    if (scalar(keys(%units_to_restart)) > 0) {
+        print STDERR "would restart the following units: ", join(", ", sort(keys(%units_to_restart))), "\n";
+    }
+    my @units_to_start_filtered = filter_units(\%units_to_start);
+    if (scalar(@units_to_start_filtered)) {
+        print STDERR "would start the following units: ", join(", ", @units_to_start_filtered), "\n";
+    }
     exit 0;
 }
 
 
 syslog(LOG_NOTICE, "switching to system configuration $out");
 
-if (scalar (keys %unitsToStop) > 0) {
-    print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n"
-        if scalar @unitsToStopFiltered;
+if (scalar(keys(%units_to_stop)) > 0) {
+    if (scalar(@units_to_stop_filtered)) {
+        print STDERR "stopping the following units: ", join(", ", @units_to_stop_filtered), "\n";
+    }
     # Use current version of systemctl binary before daemon is reexeced.
-    system("$curSystemd/systemctl", "stop", "--", sort(keys %unitsToStop));
+    system("$cur_systemd/systemctl", "stop", "--", sort(keys(%units_to_stop)));
 }
 
-print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n"
-    if scalar(keys %unitsToSkip) > 0;
+if (scalar(keys(%units_to_skip)) > 0) {
+    print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys(%units_to_skip))), "\n";
+}
 
 # Activate the new configuration (i.e., update /etc, make accounts,
 # and so on).
@@ -504,79 +762,110 @@ print STDERR "activating the configuration...\n";
 system("$out/activate", "$out") == 0 or $res = 2;
 
 # Handle the activation script requesting the restart or reload of a unit.
-foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // "")) {
+foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
     my $unit = $_;
-    my $baseUnit = $unit;
-    my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    my $base_unit = $unit;
+    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
 
     # Detect template instances.
-    if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
-      $baseUnit = "$1\@.$2";
-      $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
+      $base_unit = "$1\@.$2";
+      $new_unit_file = "$out/etc/systemd/system/$base_unit";
     }
 
-    my $baseName = $baseUnit;
-    $baseName =~ s/\.[a-z]*$//;
+    my $base_name = $base_unit;
+    $base_name =~ s/\.[[:lower:]]*$//msx;
 
     # Start units if they were not active previously
-    if (not defined $activePrev->{$unit}) {
-        $unitsToStart{$unit} = 1;
-        recordUnit($startListFile, $unit);
+    if (not defined($active_cur->{$unit})) {
+        $units_to_start{$unit} = 1;
+        record_unit($start_list_file, $unit);
         next;
     }
 
-    handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+    handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
 }
 # We can remove the file now because it has been propagated to the other restart/reload files
-unlink($restartByActivationFile);
+unlink($restart_by_activation_file);
+
+foreach (split(/\n/msx, read_file($reload_by_activation_file, err_mode => "quiet") // "")) {
+    my $unit = $_;
+
+    if (defined($active_cur->{$unit}) and not $units_to_restart{$unit} and not $units_to_stop{$unit}) {
+        $units_to_reload{$unit} = 1;
+        record_unit($reload_list_file, $unit);
+    }
+}
+# We can remove the file now because it has been propagated to the other reload file
+unlink($reload_by_activation_file);
 
 # Restart systemd if necessary. Note that this is done using the
 # current version of systemd, just in case the new one has trouble
 # communicating with the running pid 1.
-if ($restartSystemd) {
+if ($restart_systemd) {
     print STDERR "restarting systemd...\n";
-    system("$curSystemd/systemctl", "daemon-reexec") == 0 or $res = 2;
+    system("$cur_systemd/systemctl", "daemon-reexec") == 0 or $res = 2;
 }
 
 # Forget about previously failed services.
-system("@systemd@/bin/systemctl", "reset-failed");
+system("$new_systemd/bin/systemctl", "reset-failed");
 
 # Make systemd reload its units.
-system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3;
+system("$new_systemd/bin/systemctl", "daemon-reload") == 0 or $res = 3;
 
 # Reload user units
-open my $listActiveUsers, '-|', '@systemd@/bin/loginctl', 'list-users', '--no-legend';
-while (my $f = <$listActiveUsers>) {
-    next unless $f =~ /^\s*(?<uid>\d+)\s+(?<user>\S+)/;
+open(my $list_active_users, "-|", "$new_systemd/bin/loginctl", "list-users", "--no-legend") || die("Unable to call loginctl");
+while (my $f = <$list_active_users>) {
+    if ($f !~ /^\s*(?<uid>\d+)\s+(?<user>\S+)/msx) {
+        next;
+    }
     my ($uid, $name) = ($+{uid}, $+{user});
     print STDERR "reloading user units for $name...\n";
 
     system("@su@", "-s", "@shell@", "-l", $name, "-c",
            "export XDG_RUNTIME_DIR=/run/user/$uid; " .
-           "$curSystemd/systemctl --user daemon-reexec; " .
-           "@systemd@/bin/systemctl --user start nixos-activation.service");
+           "$cur_systemd/systemctl --user daemon-reexec; " .
+           "$new_systemd/bin/systemctl --user start nixos-activation.service");
 }
 
-close $listActiveUsers;
+close($list_active_users) || die("Unable to close the file handle to loginctl");
 
 # Set the new tmpfiles
 print STDERR "setting up tmpfiles\n";
-system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3;
-
+system("$new_systemd/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3;
+
+# Before reloading we need to ensure that the units are still active. They may have been
+# deactivated because one of their requirements got stopped. If they are inactive
+# but should have been reloaded, the user probably expects them to be started.
+if (scalar(keys(%units_to_reload)) > 0) {
+    for my $unit (keys(%units_to_reload)) {
+        if (!unit_is_active($unit)) {
+            # Figure out if we need to start the unit
+            my %unit_info = parse_unit("$out/etc/systemd/system/$unit");
+            if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
+                $units_to_start{$unit} = 1;
+                record_unit($start_list_file, $unit);
+            }
+            # Don't reload the unit, reloading would fail
+            delete %units_to_reload{$unit};
+            unrecord_unit($reload_list_file, $unit);
+        }
+    }
+}
 # Reload units that need it. This includes remounting changed mount
 # units.
-if (scalar(keys %unitsToReload) > 0) {
-    print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n";
-    system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4;
-    unlink($reloadListFile);
+if (scalar(keys(%units_to_reload)) > 0) {
+    print STDERR "reloading the following units: ", join(", ", sort(keys(%units_to_reload))), "\n";
+    system("$new_systemd/bin/systemctl", "reload", "--", sort(keys(%units_to_reload))) == 0 or $res = 4;
+    unlink($reload_list_file);
 }
 
 # Restart changed services (those that have to be restarted rather
 # than stopped and started).
-if (scalar(keys %unitsToRestart) > 0) {
-    print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n";
-    system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4;
-    unlink($restartListFile);
+if (scalar(keys(%units_to_restart)) > 0) {
+    print STDERR "restarting the following units: ", join(", ", sort(keys(%units_to_restart))), "\n";
+    system("$new_systemd/bin/systemctl", "restart", "--", sort(keys(%units_to_restart))) == 0 or $res = 4;
+    unlink($restart_list_file);
 }
 
 # Start all active targets, as well as changed units we stopped above.
@@ -585,29 +874,32 @@ if (scalar(keys %unitsToRestart) > 0) {
 # that are symlinks to other units.  We shouldn't start both at the
 # same time because we'll get a "Failed to add path to set" error from
 # systemd.
-my @unitsToStartFiltered = filterUnits(\%unitsToStart);
-print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n"
-    if scalar @unitsToStartFiltered;
-system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4;
-unlink($startListFile);
+my @units_to_start_filtered = filter_units(\%units_to_start);
+if (scalar(@units_to_start_filtered)) {
+    print STDERR "starting the following units: ", join(", ", @units_to_start_filtered), "\n"
+}
+system("$new_systemd/bin/systemctl", "start", "--", sort(keys(%units_to_start))) == 0 or $res = 4;
+unlink($start_list_file);
 
 
 # Print failed and new units.
 my (@failed, @new);
-my $activeNew = getActiveUnits;
-while (my ($unit, $state) = each %{$activeNew}) {
+my $active_new = get_active_units();
+while (my ($unit, $state) = each(%{$active_new})) {
     if ($state->{state} eq "failed") {
-        push @failed, $unit;
+        push(@failed, $unit);
         next;
     }
 
     if ($state->{substate} eq "auto-restart") {
         # A unit in auto-restart substate is a failure *if* it previously failed to start
-        my $main_status = `@systemd@/bin/systemctl show --value --property=ExecMainStatus '$unit'`;
+        open(my $main_status_fd, "-|", "$new_systemd/bin/systemctl", "show", "--value", "--property=ExecMainStatus", $unit) || die("Unable to call 'systemctl show'");
+        my $main_status = do { local $/ = undef; <$main_status_fd> };
+        close($main_status_fd) || die("Unable to close 'systemctl show' fd");
         chomp($main_status);
 
         if ($main_status ne "0") {
-            push @failed, $unit;
+            push(@failed, $unit);
             next;
         }
     }
@@ -615,19 +907,19 @@ while (my ($unit, $state) = each %{$activeNew}) {
     # Ignore scopes since they are not managed by this script but rather
     # created and managed by third-party services via the systemd dbus API.
     # This only lists units that are not failed (including ones that are in auto-restart but have not failed previously)
-    if ($state->{state} ne "failed" && !defined $activePrev->{$unit} && $unit !~ /\.scope$/msx) {
-        push @new, $unit;
+    if ($state->{state} ne "failed" && !defined($active_cur->{$unit}) && $unit !~ /\.scope$/msx) {
+        push(@new, $unit);
     }
 }
 
-if (scalar @new > 0) {
+if (scalar(@new) > 0) {
     print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n"
 }
 
-if (scalar @failed > 0) {
-    my @failed_sorted = sort @failed;
+if (scalar(@failed) > 0) {
+    my @failed_sorted = sort(@failed);
     print STDERR "warning: the following units failed: ", join(", ", @failed_sorted), "\n\n";
-    system "@systemd@/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2";
+    system("$new_systemd/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2");
     $res = 4;
 }
 
@@ -637,4 +929,4 @@ if ($res == 0) {
     syslog(LOG_ERR, "switching to system configuration $out failed (status $res)");
 }
 
-exit $res;
+exit($res);
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 9e6ca75b9da45..84f560691fc4d 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -55,11 +55,18 @@ let
       substituteInPlace $out/dry-activate --subst-var out
       chmod u+x $out/activate $out/dry-activate
       unset activationScript dryActivationScript
-      ${pkgs.stdenv.shellDryRun} $out/activate
-      ${pkgs.stdenv.shellDryRun} $out/dry-activate
 
-      cp ${config.system.build.bootStage2} $out/init
-      substituteInPlace $out/init --subst-var-by systemConfig $out
+      ${if config.boot.initrd.systemd.enable then ''
+        cp ${config.system.build.bootStage2} $out/prepare-root
+        substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
+        # This must not be a symlink or the abs_path of the grub builder for the tests
+        # will resolve the symlink and we end up with a path that doesn't point to a
+        # system closure.
+        cp "$systemd/lib/systemd/systemd" $out/init
+      '' else ''
+        cp ${config.system.build.bootStage2} $out/init
+        substituteInPlace $out/init --subst-var-by systemConfig $out
+      ''}
 
       ln -s ${config.system.build.etc}/etc $out/etc
       ln -s ${config.system.path} $out/sw
@@ -117,7 +124,7 @@ let
     configurationName = config.boot.loader.grub.configurationName;
 
     # Needed by switch-to-configuration.
-    perl = pkgs.perl.withPackages (p: with p; [ FileSlurp NetDBus XMLParser XMLTwig ConfigIniFiles ]);
+    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp NetDBus ]);
   };
 
   # Handle assertions and warnings
@@ -156,7 +163,7 @@ in
 
     specialisation = mkOption {
       default = {};
-      example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }";
+      example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }; }";
       description = ''
         Additional configurations to build. If
         <literal>inheritParentConfig</literal> is true, the system
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
index d147155d796c1..b2c92a85f7abb 100644
--- a/nixos/modules/system/boot/kernel.nix
+++ b/nixos/modules/system/boot/kernel.nix
@@ -36,7 +36,7 @@ in
 
     boot.kernelPackages = mkOption {
       default = pkgs.linuxPackages;
-      type = types.unspecified // { merge = mergeEqualOption; };
+      type = types.raw;
       apply = kernelPackages: kernelPackages.extend (self: super: {
         kernel = super.kernel.override (originalArgs: {
           inherit randstructSeed;
@@ -241,7 +241,7 @@ in
             "xhci_pci"
             "usbhid"
             "hid_generic" "hid_lenovo" "hid_apple" "hid_roccat"
-            "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft"
+            "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft" "hid_cherry"
 
           ] ++ optionals pkgs.stdenv.hostPlatform.isx86 [
             # Misc. x86 keyboard stuff.
@@ -273,9 +273,6 @@ in
 
         boot.kernelModules = [ "loop" "atkbd" ];
 
-        # The Linux kernel >= 2.6.27 provides firmware.
-        hardware.firmware = [ kernel ];
-
         # Create /etc/modules-load.d/nixos.conf, which is read by
         # systemd-modules-load.service to load required kernel modules.
         environment.etc =
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 8db271f871352..1f915d1f419cc 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -44,6 +44,8 @@ let
     { splashImage = f cfg.splashImage;
       splashMode = f cfg.splashMode;
       backgroundColor = f cfg.backgroundColor;
+      entryOptions = f cfg.entryOptions;
+      subEntryOptions = f cfg.subEntryOptions;
       grub = f grub;
       grubTarget = f (grub.grubTarget or "");
       shell = "${pkgs.runtimeShell}";
@@ -448,6 +450,30 @@ in
         '';
       };
 
+      entryOptions = mkOption {
+        default = "--class nixos --unrestricted";
+        type = types.nullOr types.str;
+        description = ''
+          Options applied to the primary NixOS menu entry.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
+      subEntryOptions = mkOption {
+        default = "--class nixos";
+        type = types.nullOr types.str;
+        description = ''
+          Options applied to the secondary NixOS submenu entry.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
       theme = mkOption {
         type = types.nullOr types.path;
         example = literalExpression "pkgs.nixos-grub2-theme";
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index 0c93b288fc654..d5f019423b642 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -64,6 +64,8 @@ my $extraEntries = get("extraEntries");
 my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true";
 my $splashImage = get("splashImage");
 my $splashMode = get("splashMode");
+my $entryOptions = get("entryOptions");
+my $subEntryOptions = get("subEntryOptions");
 my $backgroundColor = get("backgroundColor");
 my $configurationLimit = int(get("configurationLimit"));
 my $copyKernels = get("copyKernels") eq "true";
@@ -509,7 +511,7 @@ sub addEntry {
 # Add default entries.
 $conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
 
-addEntry("NixOS - Default", $defaultConfig, "--unrestricted");
+addEntry("NixOS - Default", $defaultConfig, $entryOptions);
 
 $conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
 
@@ -546,7 +548,7 @@ sub addProfile {
     my ($profile, $description) = @_;
 
     # Add entries for all generations of this profile.
-    $conf .= "submenu \"$description\" {\n" if $grubVersion == 2;
+    $conf .= "submenu \"$description\" --class submenu {\n" if $grubVersion == 2;
 
     sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
 
@@ -566,7 +568,7 @@ sub addProfile {
             -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);
+        addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions);
     }
 
     $conf .= "}\n" if $grubVersion == 2;
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
index 1023361f0b1f6..367feb5e88ec8 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -86,7 +86,7 @@ in
         type = types.nullOr types.lines;
         description = ''
           Extra options that will be appended to <literal>/boot/config.txt</literal> file.
-          For possible values, see: https://www.raspberrypi.org/documentation/configuration/config-txt/
+          For possible values, see: https://www.raspberrypi.com/documentation/computers/config_txt.html
         '';
       };
     };
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 adc8930630981..77280a9680e38 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
@@ -15,9 +15,12 @@ import re
 import datetime
 import glob
 import os.path
-from typing import Tuple, List, Optional
+from typing import NamedTuple, List, Optional
 
-SystemIdentifier = Tuple[Optional[str], int, Optional[str]]
+class SystemIdentifier(NamedTuple):
+    profile: Optional[str]
+    generation: int
+    specialisation: Optional[str]
 
 
 def copy_if_not_exists(source: str, dest: str) -> None:
@@ -151,7 +154,14 @@ def get_generations(profile: Optional[str] = None) -> List[SystemIdentifier]:
     gen_lines.pop()
 
     configurationLimit = @configurationLimit@
-    configurations: List[SystemIdentifier] = [ (profile, int(line.split()[0]), None) for line in gen_lines ]
+    configurations = [
+        SystemIdentifier(
+            profile=profile,
+            generation=int(line.split()[0]),
+            specialisation=None
+        )
+        for line in gen_lines
+    ]
     return configurations[-configurationLimit:]
 
 
@@ -160,7 +170,7 @@ def get_specialisations(profile: Optional[str], generation: int, _: Optional[str
             system_dir(profile, generation, None), "specialisation")
     if not os.path.exists(specialisations_dir):
         return []
-    return [(profile, generation, spec) for spec in os.listdir(specialisations_dir)]
+    return [SystemIdentifier(profile, generation, spec) for spec in os.listdir(specialisations_dir)]
 
 
 def remove_old_entries(gens: List[SystemIdentifier]) -> None:
@@ -194,7 +204,6 @@ def get_profiles() -> List[str]:
     else:
         return []
 
-
 def main() -> None:
     parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files')
     parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot')
@@ -234,26 +243,26 @@ def main() -> None:
         subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@"] + flags + ["install"])
     else:
         # Update bootloader to latest if needed
-        systemd_version = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
-        sdboot_status = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
+        available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
+        installed_out = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
 
         # See status_binaries() in systemd bootctl.c for code which generates this
-        m = re.search("^\W+File:.*/EFI/(BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$",
-                      sdboot_status, re.IGNORECASE | re.MULTILINE)
-
-        needs_install = False
-
-        if m is None:
-            print("could not find any previously installed systemd-boot, installing.")
-            # Let systemd-boot attempt an installation if a previous one wasn't found
-            needs_install = True
-        else:
-            sdboot_version = f'({m.group(2)})'
-            if systemd_version != sdboot_version:
-                print("updating systemd-boot from %s to %s" % (sdboot_version, systemd_version))
-                needs_install = True
-
-        if needs_install:
+        installed_match = re.search(r"^\W+File:.*/EFI/(?:BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$",
+                      installed_out, re.IGNORECASE | re.MULTILINE)
+
+        available_match = re.search(r"^\((.*)\)$", available_out)
+
+        if installed_match is None:
+            raise Exception("could not find any previously installed systemd-boot")
+
+        if available_match is None:
+            raise Exception("could not determine systemd-boot version")
+
+        installed_version = installed_match.group(1)
+        available_version = available_match.group(1)
+
+        if installed_version < available_version:
+            print("updating systemd-boot from %s to %s" % (installed_version, available_version))
             subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
@@ -271,7 +280,8 @@ def main() -> None:
             if os.readlink(system_dir(*gen)) == args.default_config:
                 write_loader_conf(*gen)
         except OSError as e:
-            print("ignoring generation '{}' in the list of boot entries because of the following error:\n{}".format(*gen, e), file=sys.stderr)
+            profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
+            print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
 
     for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
         relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index c07567ec82ead..1a1dcaea9c89a 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -33,7 +33,7 @@ let
     netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
 
     copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
-      empty_file=$(mktemp)
+      empty_file=$(${pkgs.coreutils}/bin/mktemp)
 
       ${concatStrings (mapAttrsToList (n: v: ''
         ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index f0d3170dc5ac2..4103a7af57cdf 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -1,10 +1,11 @@
-{ config, lib, pkgs, ... }:
+{ config, options, lib, pkgs, ... }:
 
 with lib;
 
 let
   luks = config.boot.initrd.luks;
   kernelPackages = config.boot.kernelPackages;
+  defaultPrio = (mkOptionDefault {}).priority;
 
   commonFunctions = ''
     die() {
@@ -474,6 +475,16 @@ let
   preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
   postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
 
+  stage1Crypttab = pkgs.writeText "initrd-crypttab" (lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: let
+    opts = v.crypttabExtraOpts
+      ++ optional v.allowDiscards "discard"
+      ++ optionals v.bypassWorkqueues [ "no-read-workqueue" "no-write-workqueue" ]
+      ++ optional (v.header != null) "header=${v.header}"
+      ++ optional (v.keyFileOffset != null) "keyfile-offset=${v.keyFileOffset}"
+      ++ optional (v.keyFileSize != null) "keyfile-size=${v.keyFileSize}"
+    ;
+  in "${n} ${v.device} ${if v.keyFile == null then "-" else v.keyFile} ${lib.concatStringsSep "," opts}") luks.devices));
+
 in
 {
   imports = [
@@ -802,6 +813,18 @@ in
               Commands that should be run right after we have mounted our LUKS device.
             '';
           };
+
+          crypttabExtraOpts = mkOption {
+            type = with types; listOf singleLineStr;
+            default = [];
+            example = [ "_netdev" ];
+            visible = false;
+            description = ''
+              Only used with systemd stage 1.
+
+              Extra options to append to the last column of the generated crypttab file.
+            '';
+          };
         };
       }));
     };
@@ -853,6 +876,31 @@ in
                       -> versionAtLeast kernelPackages.kernel.version "5.9";
           message = "boot.initrd.luks.devices.<name>.bypassWorkqueues is not supported for kernels older than 5.9";
         }
+
+        { assertion = config.boot.initrd.systemd.enable -> all (dev: !dev.fallbackToPassword) (attrValues luks.devices);
+          message = "boot.initrd.luks.devices.<name>.fallbackToPassword is implied by systemd stage 1.";
+        }
+        { assertion = config.boot.initrd.systemd.enable -> all (dev: dev.preLVM) (attrValues luks.devices);
+          message = "boot.initrd.luks.devices.<name>.preLVM is not used by systemd stage 1.";
+        }
+        { assertion = config.boot.initrd.systemd.enable -> options.boot.initrd.luks.reusePassphrases.highestPrio == defaultPrio;
+          message = "boot.initrd.luks.reusePassphrases has no effect with systemd stage 1.";
+        }
+        { assertion = config.boot.initrd.systemd.enable -> all (dev: dev.preOpenCommands == "" && dev.postOpenCommands == "") (attrValues luks.devices);
+          message = "boot.initrd.luks.devices.<name>.preOpenCommands and postOpenCommands is not supported by systemd stage 1. Please bind a service to cryptsetup.target or cryptsetup-pre.target instead.";
+        }
+        # TODO
+        { assertion = config.boot.initrd.systemd.enable -> !luks.gpgSupport;
+          message = "systemd stage 1 does not support GPG smartcards yet.";
+        }
+        # TODO
+        { assertion = config.boot.initrd.systemd.enable -> !luks.fido2Support;
+          message = "systemd stage 1 does not support FIDO2 yet.";
+        }
+        # TODO
+        { assertion = config.boot.initrd.systemd.enable -> !luks.yubikeySupport;
+          message = "systemd stage 1 does not support Yubikeys yet.";
+        }
       ];
 
     # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested
@@ -867,7 +915,7 @@ in
       ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
 
     # copy the cryptsetup binary and it's dependencies
-    boot.initrd.extraUtilsCommands = ''
+    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup
       copy_bin_and_libs ${askPass}/bin/cryptsetup-askpass
       sed -i s,/bin/sh,$out/bin/sh, $out/bin/cryptsetup-askpass
@@ -877,7 +925,7 @@ in
         copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykinfo
         copy_bin_and_libs ${pkgs.openssl.bin}/bin/openssl
 
-        cc -O3 -I${pkgs.openssl.dev}/include -L${pkgs.openssl.out}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
+        cc -O3 -I${pkgs.openssl.dev}/include -L${lib.getLib pkgs.openssl}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
         strip -s pbkdf2-sha512
         copy_bin_and_libs pbkdf2-sha512
 
@@ -915,7 +963,7 @@ in
       ''}
     '';
 
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       $out/bin/cryptsetup --version
       ${optionalString luks.yubikeySupport ''
         $out/bin/ykchalresp -V
@@ -932,9 +980,28 @@ in
       ''}
     '';
 
-    boot.initrd.preFailCommands = postCommands;
-    boot.initrd.preLVMCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands;
-    boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands;
+    boot.initrd.systemd = {
+      contents."/etc/crypttab".source = stage1Crypttab;
+
+      extraBin.systemd-cryptsetup = "${config.boot.initrd.systemd.package}/lib/systemd/systemd-cryptsetup";
+
+      additionalUpstreamUnits = [
+        "cryptsetup-pre.target"
+        "cryptsetup.target"
+        "remote-cryptsetup.target"
+      ];
+      storePaths = [
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-cryptsetup"
+        "${config.boot.initrd.systemd.package}/lib/systemd/system-generators/systemd-cryptsetup-generator"
+      ];
+
+    };
+    # We do this because we need the udev rules from the package
+    boot.initrd.services.lvm.enable = true;
+
+    boot.initrd.preFailCommands = mkIf (!config.boot.initrd.systemd.enable) postCommands;
+    boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands);
+    boot.initrd.postDeviceCommands = mkIf (!config.boot.initrd.systemd.enable) (commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands);
 
     environment.systemPackages = [ pkgs.cryptsetup ];
   };
diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix
index 7426d148891f4..21be18ef866eb 100644
--- a/nixos/modules/system/boot/modprobe.nix
+++ b/nixos/modules/system/boot/modprobe.nix
@@ -34,23 +34,6 @@ with lib;
       type = types.lines;
     };
 
-    boot.initrd.extraModprobeConfig = mkOption {
-      default = "";
-      example =
-        ''
-          options zfs zfs_arc_max=1073741824
-        '';
-      description = ''
-        Does exactly the same thing as
-        <option>boot.extraModprobeConfig</option>, except
-        that the generated <filename>modprobe.conf</filename>
-        file is also included in the initrd.
-        This is useful for setting module options for kernel
-        modules that are loaded during early boot in the initrd.
-      '';
-      type = types.lines;
-    };
-
   };
 
 
@@ -67,11 +50,10 @@ with lib;
         '')}
         ${config.boot.extraModprobeConfig}
       '';
-    environment.etc."modprobe.d/nixos-initrd.conf".text = ''
-        ${config.boot.initrd.extraModprobeConfig}
-      '';
     environment.etc."modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
 
+    environment.etc."modprobe.d/systemd.conf".source = "${config.systemd.package}/lib/modprobe.d/systemd.conf";
+
     environment.systemPackages = [ pkgs.kmod ];
 
     system.activationScripts.modprobe = stringAfter ["specialfs"]
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index ac1e4ef34b46f..357c2bab993b5 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -10,6 +10,36 @@ let
 
   check = {
 
+    global = {
+      sectionNetwork = checkUnitConfig "Network" [
+        (assertOnlyFields [
+          "SpeedMeter"
+          "SpeedMeterIntervalSec"
+          "ManageForeignRoutingPolicyRules"
+          "ManageForeignRoutes"
+          "RouteTable"
+        ])
+        (assertValueOneOf "SpeedMeter" boolValues)
+        (assertInt "SpeedMeterIntervalSec")
+        (assertValueOneOf "ManageForeignRoutingPolicyRules" boolValues)
+        (assertValueOneOf "ManageForeignRoutes" boolValues)
+      ];
+
+      sectionDHCPv4 = checkUnitConfig "DHCPv4" [
+        (assertOnlyFields [
+          "DUIDType"
+          "DUIDRawData"
+        ])
+      ];
+
+      sectionDHCPv6 = checkUnitConfig "DHCPv6" [
+        (assertOnlyFields [
+          "DUIDType"
+          "DUIDRawData"
+        ])
+      ];
+    };
+
     link = {
 
       sectionLink = checkUnitConfig "Link" [
@@ -281,6 +311,8 @@ let
           "PrivateKeyFile"
           "ListenPort"
           "FirewallMark"
+          "RouteTable"
+          "RouteMetric"
         ])
         (assertInt "FirewallMark")
         (assertRange "FirewallMark" 1 4294967295)
@@ -296,6 +328,8 @@ let
           "AllowedIPs"
           "Endpoint"
           "PersistentKeepalive"
+          "RouteTable"
+          "RouteMetric"
         ])
         (assertInt "PersistentKeepalive")
         (assertRange "PersistentKeepalive" 0 65535)
@@ -418,6 +452,7 @@ let
           "AllMulticast"
           "Unmanaged"
           "RequiredForOnline"
+          "RequiredFamilyForOnline"
           "ActivationPolicy"
         ])
         (assertMacAddress "MACAddress")
@@ -437,6 +472,12 @@ let
           "enslaved"
           "routable"
         ]))
+        (assertValueOneOf "RequiredFamilyForOnline" [
+          "ipv4"
+          "ipv6"
+          "both"
+          "any"
+        ])
         (assertValueOneOf "ActivationPolicy" ([
           "up"
           "always-up"
@@ -745,6 +786,7 @@ let
           "RouteDenyList"
           "RouteAllowList"
           "DHCPv6Client"
+          "RouteMetric"
         ])
         (assertValueOneOf "UseDNS" boolValues)
         (assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
@@ -867,6 +909,44 @@ let
     };
   };
 
+  networkdOptions = {
+    networkConfig = mkOption {
+      default = {};
+      example = { SpeedMeter = true; ManageForeignRoutingPolicyRules = false; };
+      type = types.addCheck (types.attrsOf unitOption) check.global.sectionNetwork;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Network]</literal> section of the networkd config.
+        See <citerefentry><refentrytitle>networkd.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpV4Config = mkOption {
+      default = {};
+      example = { DUIDType = "vendor"; };
+      type = types.addCheck (types.attrsOf unitOption) check.global.sectionDHCPv4;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPv4]</literal> section of the networkd config.
+        See <citerefentry><refentrytitle>networkd.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpV6Config = mkOption {
+      default = {};
+      example = { DUIDType = "vendor"; };
+      type = types.addCheck (types.attrsOf unitOption) check.global.sectionDHCPv6;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPv6]</literal> section of the networkd config.
+        See <citerefentry><refentrytitle>networkd.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+  };
+
   linkOptions = commonNetworkOptions // {
     # overwrite enable option from above
     enable = mkOption {
@@ -1515,6 +1595,39 @@ let
     };
   };
 
+  networkdConfig = { config, ... }: {
+    options = {
+      routeTables = mkOption {
+        default = {};
+        example = { foo = 27; };
+        type = with types; attrsOf int;
+        description = ''
+          Defines route table names as an attrset of name to number.
+          See <citerefentry><refentrytitle>networkd.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+
+      addRouteTablesToIPRoute2 = mkOption {
+        default = true;
+        example = false;
+        type = types.bool;
+        description = ''
+          If true and routeTables are set, then the specified route tables
+          will also be installed into /etc/iproute2/rt_tables.
+        '';
+      };
+    };
+
+    config = {
+      networkConfig = optionalAttrs (config.routeTables != { }) {
+        RouteTable = mapAttrsToList
+          (name: number: "${name}:${toString number}")
+          config.routeTables;
+      };
+    };
+  };
+
   commonMatchText = def: optionalString (def.matchConfig != { }) ''
     [Match]
     ${attrsToSection def.matchConfig}
@@ -1596,6 +1709,20 @@ let
         + def.extraConfig;
     };
 
+  renderConfig = def:
+    { text = ''
+        [Network]
+        ${attrsToSection def.networkConfig}
+      ''
+      + optionalString (def.dhcpV4Config != { }) ''
+        [DHCPv4]
+        ${attrsToSection def.dhcpV4Config}
+      ''
+      + optionalString (def.dhcpV6Config != { }) ''
+        [DHCPv6]
+        ${attrsToSection def.dhcpV6Config}
+      ''; };
+
   networkToUnit = name: def:
     { inherit (def) enable;
       text = commonMatchText def
@@ -1728,6 +1855,12 @@ in
       description = "Definition of systemd networks.";
     };
 
+    systemd.network.config = mkOption {
+      default = {};
+      type = with types; submodule [ { options = networkdOptions; } networkdConfig ];
+      description = "Definition of global systemd network config.";
+    };
+
     systemd.network.units = mkOption {
       description = "Definition of networkd units.";
       default = {};
@@ -1741,6 +1874,48 @@ in
         }));
     };
 
+    systemd.network.wait-online = {
+      anyInterface = mkOption {
+        description = ''
+          Whether to consider the network online when any interface is online, as opposed to all of them.
+          This is useful on portable machines with a wired and a wireless interface, for example.
+        '';
+        type = types.bool;
+        default = false;
+      };
+
+      ignoredInterfaces = mkOption {
+        description = ''
+          Network interfaces to be ignored when deciding if the system is online.
+        '';
+        type = with types; listOf str;
+        default = [];
+        example = [ "wg0" ];
+      };
+
+      timeout = mkOption {
+        description = ''
+          Time to wait for the network to come online, in seconds. Set to 0 to disable.
+        '';
+        type = types.ints.unsigned;
+        default = 120;
+        example = 0;
+      };
+
+      extraArgs = mkOption {
+        description = ''
+          Extra command-line arguments to pass to systemd-networkd-wait-online.
+          These also affect per-interface <literal>systemd-network-wait-online@</literal> services.
+
+          See <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html">
+          <citerefentry><refentrytitle>systemd-networkd-wait-online.service</refentrytitle><manvolnum>8</manvolnum>
+          </citerefentry></link> for all available options.
+        '';
+        type = with types; listOf str;
+        default = [];
+      };
+    };
+
   };
 
   config = mkMerge [
@@ -1749,6 +1924,11 @@ in
     {
       systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.links;
       environment.etc = unitFiles;
+
+      systemd.network.wait-online.extraArgs =
+        [ "--timeout=${toString cfg.wait-online.timeout}" ]
+        ++ optional cfg.wait-online.anyInterface "--any"
+        ++ map (i: "--ignore=${i}") cfg.wait-online.ignoredInterfaces;
     }
 
     (mkIf config.systemd.network.enable {
@@ -1772,11 +1952,17 @@ in
       systemd.services.systemd-networkd = {
         wantedBy = [ "multi-user.target" ];
         aliases = [ "dbus-org.freedesktop.network1.service" ];
-        restartTriggers = map (x: x.source) (attrValues unitFiles);
+        restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
+          config.environment.etc."systemd/networkd.conf".source
+        ];
       };
 
       systemd.services.systemd-networkd-wait-online = {
         wantedBy = [ "network-online.target" ];
+        serviceConfig.ExecStart = [
+          ""
+          "${config.systemd.package}/lib/systemd/systemd-networkd-wait-online ${utils.escapeSystemdExecArgs cfg.wait-online.extraArgs}"
+        ];
       };
 
       systemd.services."systemd-network-wait-online@" = {
@@ -1787,10 +1973,21 @@ in
         serviceConfig = {
           Type = "oneshot";
           RemainAfterExit = true;
-          ExecStart = "${config.systemd.package}/lib/systemd/systemd-networkd-wait-online -i %I";
+          ExecStart = "${config.systemd.package}/lib/systemd/systemd-networkd-wait-online -i %I ${utils.escapeSystemdExecArgs cfg.wait-online.extraArgs}";
         };
       };
 
+      environment.etc."systemd/networkd.conf" = renderConfig cfg.config;
+
+      networking.iproute2 = mkIf (cfg.config.addRouteTablesToIPRoute2 && cfg.config.routeTables != { }) {
+        enable = mkDefault true;
+        rttablesExtraConfig = ''
+
+          # Extra tables defined in NixOS systemd.networkd.config.routeTables.
+          ${concatStringsSep "\n" (mapAttrsToList (name: number: "${toString number} ${name}") cfg.config.routeTables)}
+        '';
+      };
+
       services.resolved.enable = mkDefault true;
     })
   ];
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 78ae8e9d20b77..59037d4e6b4d8 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -4,7 +4,10 @@ with lib;
 
 let
 
-  inherit (pkgs) plymouth nixos-icons;
+  inherit (pkgs) nixos-icons;
+  plymouth = pkgs.plymouth.override {
+    systemd = config.boot.initrd.systemd.package;
+  };
 
   cfg = config.boot.plymouth;
   opt = options.boot.plymouth;
@@ -143,7 +146,88 @@ in
     systemd.services.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
     systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
 
-    boot.initrd.extraUtilsCommands = ''
+    boot.initrd.systemd = {
+      extraBin.plymouth = "${plymouth}/bin/plymouth"; # for the recovery shell
+      storePaths = [
+        "${lib.getBin config.boot.initrd.systemd.package}/bin/systemd-tty-ask-password-agent"
+        "${plymouth}/bin/plymouthd"
+        "${plymouth}/sbin/plymouthd"
+      ];
+      packages = [ plymouth ]; # systemd units
+      contents = {
+        # Files
+        "/etc/plymouth/plymouthd.conf".source = configFile;
+        "/etc/plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
+        "/etc/plymouth/logo.png".source = cfg.logo;
+        # Directories
+        "/etc/plymouth/plugins".source = pkgs.runCommand "plymouth-initrd-plugins" {} ''
+          # Check if the actual requested theme is here
+          if [[ ! -d ${themesEnv}/share/plymouth/themes/${cfg.theme} ]]; then
+              echo "The requested theme: ${cfg.theme} is not provided by any of the packages in boot.plymouth.themePackages"
+              exit 1
+          fi
+
+          moduleName="$(sed -n 's,ModuleName *= *,,p' ${themesEnv}/share/plymouth/themes/${cfg.theme}/${cfg.theme}.plymouth)"
+
+          mkdir -p $out/renderers
+          # module might come from a theme
+          cp ${themesEnv}/lib/plymouth/{text,details,label,$moduleName}.so $out
+          cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/renderers
+        '';
+        "/etc/plymouth/themes".source = pkgs.runCommand "plymouth-initrd-themes" {} ''
+          # Check if the actual requested theme is here
+          if [[ ! -d ${themesEnv}/share/plymouth/themes/${cfg.theme} ]]; then
+              echo "The requested theme: ${cfg.theme} is not provided by any of the packages in boot.plymouth.themePackages"
+              exit 1
+          fi
+
+          mkdir $out
+          cp -r ${themesEnv}/share/plymouth/themes/${cfg.theme} $out
+          # Copy more themes if the theme depends on others
+          for theme in $(grep -hRo '/etc/plymouth/themes/.*$' ${themesEnv} | xargs -n1 basename); do
+              if [[ -d "${themesEnv}/theme" ]]; then
+                  cp -r "${themesEnv}/theme" $out
+              fi
+          done
+        '';
+
+        # Fonts
+        "/etc/plymouth/fonts".source = pkgs.runCommand "plymouth-initrd-fonts" {} ''
+          mkdir -p $out
+          cp ${cfg.font} $out
+        '';
+        "/etc/fonts/fonts.conf".text = ''
+          <?xml version="1.0"?>
+          <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+          <fontconfig>
+              <dir>/etc/plymouth/fonts</dir>
+          </fontconfig>
+        '';
+      };
+      # Properly enable units. These are the units that arch copies
+      services = {
+        plymouth-halt.wantedBy = [ "halt.target" ];
+        plymouth-kexec.wantedBy = [ "kexec.target" ];
+        plymouth-poweroff.wantedBy = [ "poweroff.target" ];
+        plymouth-quit-wait.wantedBy = [ "multi-user.target" ];
+        plymouth-quit.wantedBy = [ "multi-user.target" ];
+        plymouth-read-write.wantedBy = [ "sysinit.target" ];
+        plymouth-reboot.wantedBy = [ "reboot.target" ];
+        plymouth-start.wantedBy = [ "initrd-switch-root.target" "sysinit.target" ];
+        plymouth-switch-root-initramfs.wantedBy = [ "halt.target" "kexec.target" "plymouth-switch-root-initramfs.service" "poweroff.target" "reboot.target" ];
+        plymouth-switch-root.wantedBy = [ "initrd-switch-root.target" ];
+      };
+    };
+
+    # Insert required udev rules. We take stage 2 systemd because the udev
+    # rules are only generated when building with logind.
+    boot.initrd.services.udev.packages = [ (pkgs.runCommand "initrd-plymouth-udev-rules" {} ''
+      mkdir -p $out/etc/udev/rules.d
+      cp ${config.systemd.package.out}/lib/udev/rules.d/{70-uaccess,71-seat}.rules $out/etc/udev/rules.d
+      sed -i '/loginctl/d' $out/etc/udev/rules.d/71-seat.rules
+    '') ];
+
+    boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${plymouth}/bin/plymouth
       copy_bin_and_libs ${plymouth}/bin/plymouthd
 
@@ -198,18 +282,18 @@ in
       EOF
     '';
 
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       $out/bin/plymouthd --help >/dev/null
       $out/bin/plymouth --help >/dev/null
     '';
 
-    boot.initrd.extraUdevRulesCommands = ''
+    boot.initrd.extraUdevRulesCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       cp ${config.systemd.package}/lib/udev/rules.d/{70-uaccess,71-seat}.rules $out
       sed -i '/loginctl/d' $out/71-seat.rules
     '';
 
     # We use `mkAfter` to ensure that LUKS password prompt would be shown earlier than the splash screen.
-    boot.initrd.preLVMCommands = mkAfter ''
+    boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (mkAfter ''
       mkdir -p /etc/plymouth
       mkdir -p /run/plymouth
       ln -s ${configFile} /etc/plymouth/plymouthd.conf
@@ -221,16 +305,16 @@ in
 
       plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
       plymouth show-splash
-    '';
+    '');
 
-    boot.initrd.postMountCommands = ''
+    boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       plymouth update-root-fs --new-root-dir="$targetRoot"
     '';
 
     # `mkBefore` to ensure that any custom prompts would be visible.
-    boot.initrd.preFailCommands = mkBefore ''
+    boot.initrd.preFailCommands = mkIf (!config.boot.initrd.systemd.enable) (mkBefore ''
       plymouth quit --wait
-    '';
+    '');
 
   };
 
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 21d3fab2f35df..0ab2a875975d6 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 let
@@ -15,7 +15,7 @@ in
     services.resolved.enable = mkOption {
       default = false;
       type = types.bool;
-      description = ''
+      description = lib.mdDoc ''
         Whether to enable the systemd DNS resolver daemon.
       '';
     };
@@ -24,7 +24,7 @@ in
       default = [ ];
       example = [ "8.8.8.8" "2001:4860:4860::8844" ];
       type = types.listOf types.str;
-      description = ''
+      description = lib.mdDoc ''
         A list of IPv4 and IPv6 addresses to use as the fallback DNS servers.
         If this option is empty, a compiled-in list of DNS servers is used instead.
       '';
@@ -35,7 +35,7 @@ in
       defaultText = literalExpression "config.networking.search";
       example = [ "example.com" ];
       type = types.listOf types.str;
-      description = ''
+      description = lib.mdDoc ''
         A list of domains. These domains are used as search suffixes
         when resolving single-label host names (domain names which
         contain no dot), in order to qualify them into fully-qualified
@@ -43,7 +43,7 @@ in
 
         For compatibility reasons, if this setting is not specified,
         the search domains listed in
-        <filename>/etc/resolv.conf</filename> are used instead, if
+        {file}`/etc/resolv.conf` are used instead, if
         that file exists and any domains are configured in it.
       '';
     };
@@ -52,32 +52,14 @@ in
       default = "true";
       example = "false";
       type = types.enum [ "true" "resolve" "false" ];
-      description = ''
+      description = lib.mdDoc ''
         Controls Link-Local Multicast Name Resolution support
         (RFC 4795) on the local host.
 
         If set to
-
-        <variablelist>
-        <varlistentry>
-          <term><literal>"true"</literal></term>
-          <listitem><para>
-            Enables full LLMNR responder and resolver support.
-          </para></listitem>
-        </varlistentry>
-        <varlistentry>
-          <term><literal>"false"</literal></term>
-          <listitem><para>
-            Disables both.
-          </para></listitem>
-        </varlistentry>
-        <varlistentry>
-          <term><literal>"resolve"</literal></term>
-          <listitem><para>
-            Only resolution support is enabled, but responding is disabled.
-          </para></listitem>
-        </varlistentry>
-        </variablelist>
+        - `"true"`: Enables full LLMNR responder and resolver support.
+        - `"false"`: Disables both.
+        - `"resolve"`: Only resolution support is enabled, but responding is disabled.
       '';
     };
 
@@ -85,21 +67,14 @@ in
       default = "allow-downgrade";
       example = "true";
       type = types.enum [ "true" "allow-downgrade" "false" ];
-      description = ''
+      description = lib.mdDoc ''
         If set to
-        <variablelist>
-        <varlistentry>
-          <term><literal>"true"</literal></term>
-          <listitem><para>
+        - `"true"`:
             all DNS lookups are DNSSEC-validated locally (excluding
             LLMNR and Multicast DNS). Note that this mode requires a
             DNS server that supports DNSSEC. If the DNS server does
             not properly support DNSSEC all validations will fail.
-          </para></listitem>
-        </varlistentry>
-        <varlistentry>
-          <term><literal>"allow-downgrade"</literal></term>
-          <listitem><para>
+        - `"allow-downgrade"`:
             DNSSEC validation is attempted, but if the server does not
             support DNSSEC properly, DNSSEC mode is automatically
             disabled. Note that this mode makes DNSSEC validation
@@ -107,22 +82,14 @@ in
             be able to trigger a downgrade to non-DNSSEC mode by
             synthesizing a DNS response that suggests DNSSEC was not
             supported.
-          </para></listitem>
-        </varlistentry>
-        <varlistentry>
-          <term><literal>"false"</literal></term>
-          <listitem><para>
-            DNS lookups are not DNSSEC validated.
-          </para></listitem>
-        </varlistentry>
-        </variablelist>
+        - `"false"`: DNS lookups are not DNSSEC validated.
       '';
     };
 
     services.resolved.extraConfig = mkOption {
       default = "";
       type = types.lines;
-      description = ''
+      description = lib.mdDoc ''
         Extra config to append to resolved.conf.
       '';
     };
@@ -178,6 +145,8 @@ in
     # If networkmanager is enabled, ask it to interface with resolved.
     networking.networkmanager.dns = "systemd-resolved";
 
+    networking.resolvconf.package = pkgs.systemd;
+
   };
 
 }
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 8fcc1f029723e..337064034efb0 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -14,6 +14,8 @@ extraUtils="@extraUtils@"
 export LD_LIBRARY_PATH=@extraUtils@/lib
 export PATH=@extraUtils@/bin
 ln -s @extraUtils@/bin /bin
+# hardcoded in util-linux's mount helper search path `/run/wrappers/bin:/run/current-system/sw/bin:/sbin`
+ln -s @extraUtils@/bin /sbin
 
 # Copy the secrets to their needed location
 if [ -d "@extraUtils@/secrets" ]; then
@@ -232,7 +234,8 @@ done
 mkdir -p /lib
 ln -s @modulesClosure@/lib/modules /lib/modules
 ln -s @modulesClosure@/lib/firmware /lib/firmware
-echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
+# see comment in stage-1.nix for explanation
+echo @extraUtils@/bin/modprobe-kernel > /proc/sys/kernel/modprobe
 for i in @kernelModules@; do
     info "loading module $(basename $i)..."
     modprobe $i
@@ -317,11 +320,7 @@ checkFS() {
 
     echo "checking $device..."
 
-    fsckFlags=
-    if test "$fsType" != "btrfs"; then
-        fsckFlags="-V -a"
-    fi
-    fsck $fsckFlags "$device"
+    fsck -V -a "$device"
     fsckResult=$?
 
     if test $(($fsckResult | 2)) = $fsckResult; then
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 1575c0257d1c6..e35ccff290786 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -31,6 +31,9 @@ let
   # mounting `/`, like `/` on a loopback).
   fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
 
+  # Determine whether zfs-mount(8) is needed.
+  zfsRequiresMountHelper = any (fs: lib.elem "zfsutil" fs.options) fileSystems;
+
   # A utility for enumerating the shared-library dependencies of a program
   findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" ''
     set -euo pipefail
@@ -107,6 +110,22 @@ let
         copy_bin_and_libs $BIN
       done
 
+      ${optionalString zfsRequiresMountHelper ''
+        # Filesystems using the "zfsutil" option are mounted regardless of the
+        # mount.zfs(8) helper, but it is required to ensure that ZFS properties
+        # are used as mount options.
+        #
+        # BusyBox does not use the ZFS helper in the first place.
+        # util-linux searches /sbin/ as last path for helpers (stage-1-init.sh
+        # must symlink it to the store PATH).
+        # Without helper program, both `mount`s silently fails back to internal
+        # code, using default options and effectively ignore security relevant
+        # ZFS properties such as `setuid=off` and `exec=off` (unless manually
+        # duplicated in `fileSystems.*.options`, defeating "zfsutil"'s purpose).
+        copy_bin_and_libs ${pkgs.util-linux}/bin/mount
+        copy_bin_and_libs ${pkgs.zfs}/bin/mount.zfs
+      ''}
+
       # Copy some util-linux stuff.
       copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid
 
@@ -131,6 +150,26 @@ let
       copy_bin_and_libs ${pkgs.kmod}/bin/kmod
       ln -sf kmod $out/bin/modprobe
 
+      # Dirty hack to make sure the kernel properly loads modules
+      # such as ext4 on demand (e.g. on a `mount(2)` syscall). This is necessary
+      # because `kmod` isn't linked against `libpthread.so.0` anymore (since
+      # it was merged into `libc.so.6` since version `2.34`), but still needs
+      # to access it for some reason. This is not an issue in stage-1 itself
+      # because of the `LD_LIBRARY_PATH`-variable and anytime later because the rpath of
+      # kmod/modprobe points to glibc's `$out/lib` where `libpthread.so.6` exists.
+      # However, this is a problem when the kernel calls `modprobe` inside
+      # the initial ramdisk because it doesn't know about the
+      # `LD_LIBRARY_PATH` and the rpath was nuked.
+      #
+      # Also, we can't use `makeWrapper` here because `kmod` only does
+      # `modprobe` functionality if `argv[0] == "modprobe"`.
+      cat >$out/bin/modprobe-kernel <<EOF
+      #!$out/bin/ash
+      export LD_LIBRARY_PATH=$out/lib
+      exec $out/bin/modprobe "\$@"
+      EOF
+      chmod +x $out/bin/modprobe-kernel
+
       # Copy resize2fs if any ext* filesystems are to be resized
       ${optionalString (any (fs: fs.autoResize && (lib.hasPrefix "ext" fs.fsType)) fileSystems) ''
         # We need mke2fs in the initrd.
@@ -184,24 +223,29 @@ let
 
       # Run patchelf to make the programs refer to the copied libraries.
       find $out/bin $out/lib -type f | while read i; do
-        if ! test -L $i; then
-          nuke-refs -e $out $i
-        fi
+        nuke-refs -e $out $i
       done
 
       find $out/bin -type f | while read i; do
-        if ! test -L $i; then
-          echo "patching $i..."
-          patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true
-        fi
+        echo "patching $i..."
+        patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true
+      done
+
+      find $out/lib -type f \! -name 'ld*.so.?' | while read i; do
+        echo "patching $i..."
+        patchelf --set-rpath $out/lib $i
       done
 
       if [ -z "${toString (pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform)}" ]; then
       # Make sure that the patchelf'ed binaries still work.
       echo "testing patched programs..."
       $out/bin/ash -c 'echo hello world' | grep "hello world"
-      export LD_LIBRARY_PATH=$out/lib
-      $out/bin/mount --help 2>&1 | grep -q "BusyBox"
+      ${if zfsRequiresMountHelper then ''
+        $out/bin/mount -V 1>&1 | grep -q "mount from util-linux"
+        $out/bin/mount.zfs -h 2>&1 | grep -q "Usage: mount.zfs"
+      '' else ''
+        $out/bin/mount --help 2>&1 | grep -q "BusyBox"
+      ''}
       $out/bin/blkid -V 2>&1 | grep -q 'libblkid'
       $out/bin/udevadm --version
       $out/bin/dmsetup --version 2>&1 | tee -a log | grep -q "version:"
@@ -240,8 +284,6 @@ let
     } ''
       mkdir -p $out
 
-      echo 'ENV{LD_LIBRARY_PATH}="${extraUtils}/lib"' > $out/00-env.rules
-
       cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/
       cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/
       cp -v ${udev}/lib/udev/rules.d/75-net-description.rules $out/
@@ -335,12 +377,9 @@ let
       [ { object = bootStage1;
           symlink = "/init";
         }
-        { object = pkgs.writeText "mdadm.conf" config.boot.initrd.mdadmConf;
+        { object = pkgs.writeText "mdadm.conf" config.boot.initrd.services.swraid.mdadmConf;
           symlink = "/etc/mdadm.conf";
         }
-        { object = config.environment.etc."modprobe.d/nixos-initrd.conf".source;
-          symlink = "/etc/modprobe.d/nixos-initrd.conf";
-        }
         { object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" {
               src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
               preferLocalBuild = true;
@@ -403,7 +442,7 @@ let
         ${lib.optionalString (config.boot.initrd.secrets == {})
             "exit 0"}
 
-        export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
+        export PATH=${pkgs.coreutils}/bin:${pkgs.libarchive}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
 
         function cleanup {
           if [ -n "$tmp" -a -d "$tmp" ]; then
@@ -423,7 +462,7 @@ let
           ) config.boot.initrd.secrets)
          }
 
-        (cd "$tmp" && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
+        (cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
           ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
       '';
 
@@ -488,14 +527,6 @@ in
       '';
     };
 
-    boot.initrd.mdadmConf = mkOption {
-      default = "";
-      type = types.lines;
-      description = ''
-        Contents of <filename>/etc/mdadm.conf</filename> in stage 1.
-      '';
-    };
-
     boot.initrd.preLVMCommands = mkOption {
       default = "";
       type = types.lines;
@@ -581,7 +612,7 @@ in
         else "gzip"
       );
       defaultText = literalDocBook "<literal>zstd</literal> if the kernel supports it (5.9+), <literal>gzip</literal> if not";
-      type = types.unspecified; # We don't have a function type...
+      type = types.either types.str (types.functionTo types.str);
       description = ''
         The compressor to use on the initrd image. May be any of:
 
@@ -706,8 +737,12 @@ in
       }
     ];
 
-    system.build =
-      { inherit bootStage1 initialRamdisk initialRamdiskSecretAppender extraUtils; };
+    system.build = mkMerge [
+      { inherit bootStage1 initialRamdiskSecretAppender extraUtils; }
+
+      # generated in nixos/modules/system/boot/systemd/initrd.nix
+      (mkIf (!config.boot.initrd.systemd.enable) { inherit initialRamdisk; })
+    ];
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isYes "TMPFS")
@@ -715,6 +750,9 @@ in
     ];
 
     boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems;
-
   };
+
+  imports = [
+    (mkRenamedOptionModule [ "boot" "initrd" "mdadmConf" ] [ "boot" "initrd" "services" "swraid" "mdadmConf" ])
+  ];
 }
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index a90f58042d2d6..f2a839d078681 100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -5,32 +5,30 @@ systemConfig=@systemConfig@
 export HOME=/root PATH="@path@"
 
 
-# Process the kernel command line.
-for o in $(</proc/cmdline); do
-    case $o in
-        boot.debugtrace)
-            # Show each command.
-            set -x
-            ;;
-        resume=*)
-            set -- $(IFS==; echo $o)
-            resumeDevice=$2
-            ;;
-    esac
-done
-
-
-# Print a greeting.
-echo
-echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
-echo
-
-
-# Normally, stage 1 mounts the root filesystem read/writable.
-# However, in some environments, stage 2 is executed directly, and the
-# root is read-only.  So make it writable here.
-if [ -z "$container" ]; then
-    mount -n -o remount,rw none /
+if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
+    # Process the kernel command line.
+    for o in $(</proc/cmdline); do
+        case $o in
+            boot.debugtrace)
+                # Show each command.
+                set -x
+                ;;
+        esac
+    done
+
+
+    # Print a greeting.
+    echo
+    echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
+    echo
+
+
+    # Normally, stage 1 mounts the root filesystem read/writable.
+    # However, in some environments, stage 2 is executed directly, and the
+    # root is read-only.  So make it writable here.
+    if [ -z "$container" ]; then
+        mount -n -o remount,rw none /
+    fi
 fi
 
 
@@ -43,6 +41,12 @@ if [ ! -e /proc/1 ]; then
         local options="$3"
         local fsType="$4"
 
+        # We must not overwrite this mount because it's bind-mounted
+        # from stage 1's /run
+        if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] && [ "${mountPoint}" = /run ]; then
+            return
+        fi
+
         install -m 0755 -d "$mountPoint"
         mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
     }
@@ -50,7 +54,11 @@ if [ ! -e /proc/1 ]; then
 fi
 
 
-echo "booting system configuration $systemConfig" > /dev/kmsg
+if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ]; then
+    echo "booting system configuration ${systemConfig}"
+else
+    echo "booting system configuration $systemConfig" > /dev/kmsg
+fi
 
 
 # Make /nix/store a read-only bind mount to enforce immutability of
@@ -72,59 +80,32 @@ if [ -n "@readOnlyStore@" ]; then
 fi
 
 
-# Provide a /etc/mtab.
-install -m 0755 -d /etc
-test -e /etc/fstab || touch /etc/fstab # to shut up mount
-rm -f /etc/mtab* # not that we care about stale locks
-ln -s /proc/mounts /etc/mtab
-
-
-# More special file systems, initialise required directories.
-[ -e /proc/bus/usb ] && mount -t usbfs usbfs /proc/bus/usb # UML doesn't have USB by default
-install -m 01777 -d /tmp
-install -m 0755 -d /var/{log,lib,db} /nix/var /etc/nixos/ \
-    /run/lock /home /bin # for the /bin/sh symlink
-
-
-# Miscellaneous boot time cleanup.
-rm -rf /var/run /var/lock
-rm -f /etc/{group,passwd,shadow}.lock
-
-
-# Also get rid of temporary GC roots.
-rm -rf /nix/var/nix/gcroots/tmp /nix/var/nix/temproots
-
-
-# For backwards compatibility, symlink /var/run to /run, and /var/lock
-# to /run/lock.
-ln -s /run /var/run
-ln -s /run/lock /var/lock
+if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
+    # Use /etc/resolv.conf supplied by systemd-nspawn, if applicable.
+    if [ -n "@useHostResolvConf@" ] && [ -e /etc/resolv.conf ]; then
+        resolvconf -m 1000 -a host </etc/resolv.conf
+    fi
 
 
-# Clear the resume device.
-if test -n "$resumeDevice"; then
-    mkswap "$resumeDevice" || echo 'Failed to clear saved image.'
+    # Log the script output to /dev/kmsg or /run/log/stage-2-init.log.
+    # Only at this point are all the necessary prerequisites ready for these commands.
+    exec {logOutFd}>&1 {logErrFd}>&2
+    if test -w /dev/kmsg; then
+        exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do
+            if test -n "$line"; then
+                echo "<7>stage-2-init: $line" > /dev/kmsg
+            fi
+        done) 2>&1
+    else
+        mkdir -p /run/log
+        exec > >(tee -i /run/log/stage-2-init.log) 2>&1
+    fi
 fi
 
 
-# Use /etc/resolv.conf supplied by systemd-nspawn, if applicable.
-if [ -n "@useHostResolvConf@" ] && [ -e /etc/resolv.conf ]; then
-    resolvconf -m 1000 -a host </etc/resolv.conf
-fi
-
-# Log the script output to /dev/kmsg or /run/log/stage-2-init.log.
-# Only at this point are all the necessary prerequisites ready for these commands.
-exec {logOutFd}>&1 {logErrFd}>&2
-if test -w /dev/kmsg; then
-    exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do
-        if test -n "$line"; then
-            echo "<7>stage-2-init: $line" > /dev/kmsg
-        fi
-    done) 2>&1
-else
-    mkdir -p /run/log
-    exec > >(tee -i /run/log/stage-2-init.log) 2>&1
-fi
+# Required by the activation script
+install -m 0755 -d /etc /etc/nixos
+install -m 01777 -d /tmp
 
 
 # Run the script that performs all configuration activation that does
@@ -133,22 +114,9 @@ echo "running activation script..."
 $systemConfig/activate
 
 
-# Restore the system time from the hardware clock.  We do this after
-# running the activation script to be sure that /etc/localtime points
-# at the current time zone.
-if [ -e /dev/rtc ]; then
-    hwclock --hctosys
-fi
-
-
 # Record the boot configuration.
 ln -sfn "$systemConfig" /run/booted-system
 
-# Prevent the booted system from being garbage-collected. If it weren't
-# a gcroot, if we were running a different kernel, switched system,
-# and garbage collected all, we could not load kernel modules anymore.
-ln -sfn /run/booted-system /nix/var/nix/gcroots/booted-system
-
 
 # Run any user-specified commands.
 @shell@ @postBootCommands@
@@ -162,15 +130,15 @@ ln -sfn /run/booted-system /nix/var/nix/gcroots/booted-system
 : >> /etc/machine-id
 
 
-# Reset the logging file descriptors.
-exec 1>&$logOutFd 2>&$logErrFd
-exec {logOutFd}>&- {logErrFd}>&-
-
+# No need to restore the stdout/stderr streams we never redirected and
+# especially no need to start systemd
+if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
+    # Reset the logging file descriptors.
+    exec 1>&$logOutFd 2>&$logErrFd
+    exec {logOutFd}>&- {logErrFd}>&-
 
-# Start systemd.
-echo "starting systemd..."
 
-PATH=/run/current-system/systemd/lib/systemd:@fsPackagesPath@ \
-    LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive @systemdUnitPathEnvVar@ \
-    TZDIR=/etc/zoneinfo \
-    exec @systemdExecutable@
+    # Start systemd in a clean environment.
+    echo "starting systemd..."
+    exec @systemdExecutable@ "$@"
+fi
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index f6b6a8e4b0b44..f6461daf31163 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -19,11 +19,6 @@ let
       pkgs.coreutils
       pkgs.util-linux
     ] ++ lib.optional useHostResolvConf pkgs.openresolv);
-    fsPackagesPath = lib.makeBinPath config.system.fsPackages;
-    systemdUnitPathEnvVar = lib.optionalString (config.boot.extraSystemdUnitPaths != [])
-      ("SYSTEMD_UNIT_PATH="
-      + builtins.concatStringsSep ":" config.boot.extraSystemdUnitPaths
-      + ":"); # If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable
     postBootCommands = pkgs.writeText "local-cmds"
       ''
         ${config.boot.postBootCommands}
@@ -47,43 +42,11 @@ in
         '';
       };
 
-      devSize = mkOption {
-        default = "5%";
-        example = "32m";
-        type = types.str;
-        description = ''
-          Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option,
-          for the accepted syntax.
-        '';
-      };
-
-      devShmSize = mkOption {
-        default = "50%";
-        example = "256m";
-        type = types.str;
-        description = ''
-          Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option,
-          for the accepted syntax.
-        '';
-      };
-
-      runSize = mkOption {
-        default = "25%";
-        example = "256m";
-        type = types.str;
-        description = ''
-          Size limit for the /run tmpfs. Look at mount(8), tmpfs size option,
-          for the accepted syntax.
-        '';
-      };
-
       systemdExecutable = mkOption {
-        default = "systemd";
+        default = "/run/current-system/systemd/lib/systemd/systemd";
         type = types.str;
         description = ''
-          The program to execute to start systemd. Typically
-          <literal>systemd</literal>, which will find systemd in the
-          PATH.
+          The program to execute to start systemd.
         '';
       };
 
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 9dcf9eb769f80..645fbc2b713a9 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -2,14 +2,22 @@
 
 with utils;
 with systemdUtils.unitOptions;
-with systemdUtils.lib;
 with lib;
 
 let
 
   cfg = config.systemd;
 
-  systemd = cfg.package;
+  inherit (systemdUtils.lib)
+    generateUnits
+    targetToUnit
+    serviceToUnit
+    socketToUnit
+    timerToUnit
+    pathToUnit
+    mountToUnit
+    automountToUnit
+    sliceToUnit;
 
   upstreamSystemUnits =
     [ # Targets.
@@ -25,11 +33,11 @@ let
       "nss-lookup.target"
       "nss-user-lookup.target"
       "time-sync.target"
-    ] ++ (optionals cfg.package.withCryptsetup [
+    ] ++ optionals cfg.package.withCryptsetup [
       "cryptsetup.target"
       "cryptsetup-pre.target"
       "remote-cryptsetup.target"
-    ]) ++ [
+    ] ++ [
       "sigpwr.target"
       "timers.target"
       "paths.target"
@@ -63,32 +71,6 @@ let
       "printer.target"
       "smartcard.target"
 
-      # Login stuff.
-      "systemd-logind.service"
-      "autovt@.service"
-      "systemd-user-sessions.service"
-      "dbus-org.freedesktop.import1.service"
-      "dbus-org.freedesktop.machine1.service"
-      "dbus-org.freedesktop.login1.service"
-      "user@.service"
-      "user-runtime-dir@.service"
-
-      # Journal.
-      "systemd-journald.socket"
-      "systemd-journald@.socket"
-      "systemd-journald-varlink@.socket"
-      "systemd-journald.service"
-      "systemd-journald@.service"
-      "systemd-journal-flush.service"
-      "systemd-journal-catalog-update.service"
-      ] ++ (optional (!config.boot.isContainer) "systemd-journald-audit.socket") ++ [
-      "systemd-journald-dev-log.socket"
-      "syslog.socket"
-
-      # Coredumps.
-      "systemd-coredump.socket"
-      "systemd-coredump@.service"
-
       # Kernel module loading.
       "systemd-modules-load.service"
       "kmod-static-nodes.service"
@@ -149,32 +131,29 @@ let
 
       # Slices / containers.
       "slices.target"
-      "user.slice"
+    ] ++ optionals cfg.package.withImportd [
+      "systemd-importd.service"
+    ] ++ optionals cfg.package.withMachined [
       "machine.slice"
       "machines.target"
-      "systemd-importd.service"
       "systemd-machined.service"
+    ] ++ [
       "systemd-nspawn@.service"
 
-      # Temporary file creation / cleanup.
-      "systemd-tmpfiles-clean.service"
-      "systemd-tmpfiles-clean.timer"
-      "systemd-tmpfiles-setup.service"
-      "systemd-tmpfiles-setup-dev.service"
-
       # Misc.
       "systemd-sysctl.service"
+    ] ++ optionals cfg.package.withTimedated [
       "dbus-org.freedesktop.timedate1.service"
-      "dbus-org.freedesktop.locale1.service"
-      "dbus-org.freedesktop.hostname1.service"
       "systemd-timedated.service"
+    ] ++ optionals cfg.package.withLocaled [
+      "dbus-org.freedesktop.locale1.service"
       "systemd-localed.service"
+    ] ++ optionals cfg.package.withHostnamed [
+      "dbus-org.freedesktop.hostname1.service"
       "systemd-hostnamed.service"
+    ] ++ [
       "systemd-exit.service"
       "systemd-update-done.service"
-    ] ++ optionals config.services.journald.enableHttpGateway [
-      "systemd-journal-gatewayd.socket"
-      "systemd-journal-gatewayd.service"
     ] ++ cfg.additionalUpstreamSystemUnits;
 
   upstreamSystemWants =
@@ -185,235 +164,6 @@ let
       "timers.target.wants"
     ];
 
-    upstreamUserUnits = [
-      "app.slice"
-      "background.slice"
-      "basic.target"
-      "bluetooth.target"
-      "default.target"
-      "exit.target"
-      "graphical-session-pre.target"
-      "graphical-session.target"
-      "paths.target"
-      "printer.target"
-      "session.slice"
-      "shutdown.target"
-      "smartcard.target"
-      "sockets.target"
-      "sound.target"
-      "systemd-exit.service"
-      "systemd-tmpfiles-clean.service"
-      "systemd-tmpfiles-clean.timer"
-      "systemd-tmpfiles-setup.service"
-      "timers.target"
-      "xdg-desktop-autostart.target"
-    ];
-
-  makeJobScript = name: text:
-    let
-      scriptName = replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
-      out = (pkgs.writeShellScriptBin scriptName ''
-        set -e
-        ${text}
-      '').overrideAttrs (_: {
-        # The derivation name is different from the script file name
-        # to keep the script file name short to avoid cluttering logs.
-        name = "unit-script-${scriptName}";
-      });
-    in "${out}/bin/${scriptName}";
-
-  unitConfig = { config, options, ... }: {
-    config = {
-      unitConfig =
-        optionalAttrs (config.requires != [])
-          { Requires = toString config.requires; }
-        // optionalAttrs (config.wants != [])
-          { Wants = toString config.wants; }
-        // optionalAttrs (config.after != [])
-          { After = toString config.after; }
-        // optionalAttrs (config.before != [])
-          { Before = toString config.before; }
-        // optionalAttrs (config.bindsTo != [])
-          { BindsTo = toString config.bindsTo; }
-        // optionalAttrs (config.partOf != [])
-          { PartOf = toString config.partOf; }
-        // optionalAttrs (config.conflicts != [])
-          { Conflicts = toString config.conflicts; }
-        // optionalAttrs (config.requisite != [])
-          { Requisite = toString config.requisite; }
-        // optionalAttrs (config.restartTriggers != [])
-          { X-Restart-Triggers = toString config.restartTriggers; }
-        // optionalAttrs (config.description != "") {
-          Description = config.description; }
-        // optionalAttrs (config.documentation != []) {
-          Documentation = toString config.documentation; }
-        // optionalAttrs (config.onFailure != []) {
-          OnFailure = toString config.onFailure; }
-        // optionalAttrs (options.startLimitIntervalSec.isDefined) {
-          StartLimitIntervalSec = toString config.startLimitIntervalSec;
-        } // optionalAttrs (options.startLimitBurst.isDefined) {
-          StartLimitBurst = toString config.startLimitBurst;
-        };
-    };
-  };
-
-  serviceConfig = { name, config, ... }: {
-    config = mkMerge
-      [ { # Default path for systemd services.  Should be quite minimal.
-          path = mkAfter
-            [ pkgs.coreutils
-              pkgs.findutils
-              pkgs.gnugrep
-              pkgs.gnused
-              systemd
-            ];
-          environment.PATH = "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
-        }
-        (mkIf (config.preStart != "")
-          { serviceConfig.ExecStartPre =
-              [ (makeJobScript "${name}-pre-start" config.preStart) ];
-          })
-        (mkIf (config.script != "")
-          { serviceConfig.ExecStart =
-              makeJobScript "${name}-start" config.script + " " + config.scriptArgs;
-          })
-        (mkIf (config.postStart != "")
-          { serviceConfig.ExecStartPost =
-              [ (makeJobScript "${name}-post-start" config.postStart) ];
-          })
-        (mkIf (config.reload != "")
-          { serviceConfig.ExecReload =
-              makeJobScript "${name}-reload" config.reload;
-          })
-        (mkIf (config.preStop != "")
-          { serviceConfig.ExecStop =
-              makeJobScript "${name}-pre-stop" config.preStop;
-          })
-        (mkIf (config.postStop != "")
-          { serviceConfig.ExecStopPost =
-              makeJobScript "${name}-post-stop" config.postStop;
-          })
-      ];
-  };
-
-  mountConfig = { config, ... }: {
-    config = {
-      mountConfig =
-        { What = config.what;
-          Where = config.where;
-        } // optionalAttrs (config.type != "") {
-          Type = config.type;
-        } // optionalAttrs (config.options != "") {
-          Options = config.options;
-        };
-    };
-  };
-
-  automountConfig = { config, ... }: {
-    config = {
-      automountConfig =
-        { Where = config.where;
-        };
-    };
-  };
-
-  commonUnitText = def: ''
-      [Unit]
-      ${attrsToSection def.unitConfig}
-    '';
-
-  targetToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text =
-        ''
-          [Unit]
-          ${attrsToSection def.unitConfig}
-        '';
-    };
-
-  serviceToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Service]
-          ${let env = cfg.globalEnvironment // def.environment;
-            in concatMapStrings (n:
-              let s = optionalString (env.${n} != null)
-                "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
-              # systemd max line length is now 1MiB
-              # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
-              in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
-          ${if def.reloadIfChanged then ''
-            X-ReloadIfChanged=true
-          '' else if !def.restartIfChanged then ''
-            X-RestartIfChanged=false
-          '' else ""}
-          ${optionalString (!def.stopIfChanged) "X-StopIfChanged=false"}
-          ${attrsToSection def.serviceConfig}
-        '';
-    };
-
-  socketToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Socket]
-          ${attrsToSection def.socketConfig}
-          ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
-          ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
-        '';
-    };
-
-  timerToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Timer]
-          ${attrsToSection def.timerConfig}
-        '';
-    };
-
-  pathToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Path]
-          ${attrsToSection def.pathConfig}
-        '';
-    };
-
-  mountToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Mount]
-          ${attrsToSection def.mountConfig}
-        '';
-    };
-
-  automountToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Automount]
-          ${attrsToSection def.automountConfig}
-        '';
-    };
-
-  sliceToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
-      text = commonUnitText def +
-        ''
-          [Slice]
-          ${attrsToSection def.sliceConfig}
-        '';
-    };
-
-  logindHandlerType = types.enum [
-    "ignore" "poweroff" "reboot" "halt" "kexec" "suspend"
-    "hibernate" "hybrid-sleep" "suspend-then-hibernate" "lock"
-  ];
-
   proxy_env = config.networking.proxy.envVars;
 
 in
@@ -433,13 +183,7 @@ in
     systemd.units = mkOption {
       description = "Definition of systemd units.";
       default = {};
-      type = with types; attrsOf (submodule (
-        { name, config, ... }:
-        { options = concreteUnitOptions;
-          config = {
-            unit = mkDefault (makeUnit name config);
-          };
-        }));
+      type = systemdUtils.types.units;
     };
 
     systemd.packages = mkOption {
@@ -451,37 +195,37 @@ in
 
     systemd.targets = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] );
+      type = systemdUtils.types.targets;
       description = "Definition of systemd target units.";
     };
 
     systemd.services = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ]);
+      type = systemdUtils.types.services;
       description = "Definition of systemd service units.";
     };
 
     systemd.sockets = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ]);
+      type = systemdUtils.types.sockets;
       description = "Definition of systemd socket units.";
     };
 
     systemd.timers = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ]);
+      type = systemdUtils.types.timers;
       description = "Definition of systemd timer units.";
     };
 
     systemd.paths = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]);
+      type = systemdUtils.types.paths;
       description = "Definition of systemd path units.";
     };
 
     systemd.mounts = mkOption {
       default = [];
-      type = with types; listOf (submodule [ { options = mountOptions; } unitConfig mountConfig ]);
+      type = systemdUtils.types.mounts;
       description = ''
         Definition of systemd mount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -491,7 +235,7 @@ in
 
     systemd.automounts = mkOption {
       default = [];
-      type = with types; listOf (submodule [ { options = automountOptions; } unitConfig automountConfig ]);
+      type = systemdUtils.types.automounts;
       description = ''
         Definition of systemd automount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -501,7 +245,7 @@ in
 
     systemd.slices = mkOption {
       default = {};
-      type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig] );
+      type = systemdUtils.types.slices;
       description = "Definition of slice configurations.";
     };
 
@@ -550,39 +294,29 @@ in
       '';
     };
 
-    systemd.enableCgroupAccounting = mkOption {
-      default = true;
-      type = types.bool;
+    systemd.managerEnvironment = mkOption {
+      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+      default = {};
+      example = { SYSTEMD_LOG_LEVEL = "debug"; };
       description = ''
-        Whether to enable cgroup accounting.
+        Environment variables of PID 1. These variables are
+        <emphasis>not</emphasis> passed to started units.
       '';
     };
 
-    systemd.enableUnifiedCgroupHierarchy = mkOption {
+    systemd.enableCgroupAccounting = mkOption {
       default = true;
       type = types.bool;
       description = ''
-        Whether to enable the unified cgroup hierarchy (cgroupsv2).
+        Whether to enable cgroup accounting.
       '';
     };
 
-    systemd.coredump.enable = mkOption {
+    systemd.enableUnifiedCgroupHierarchy = mkOption {
       default = true;
       type = types.bool;
       description = ''
-        Whether core dumps should be processed by
-        <command>systemd-coredump</command>. If disabled, core dumps
-        appear in the current directory of the crashing process.
-      '';
-    };
-
-    systemd.coredump.extraConfig = mkOption {
-      default = "";
-      type = types.lines;
-      example = "Storage=journal";
-      description = ''
-        Extra config options for systemd-coredump. See coredump.conf(5) man page
-        for available options.
+        Whether to enable the unified cgroup hierarchy (cgroupsv2).
       '';
     };
 
@@ -596,142 +330,6 @@ in
       '';
     };
 
-    services.journald.console = mkOption {
-      default = "";
-      type = types.str;
-      description = "If non-empty, write log messages to the specified TTY device.";
-    };
-
-    services.journald.rateLimitInterval = mkOption {
-      default = "30s";
-      type = types.str;
-      description = ''
-        Configures the rate limiting interval that is applied to all
-        messages generated on the system. This rate limiting is applied
-        per-service, so that two services which log do not interfere with
-        each other's limit. The value may be specified in the following
-        units: s, min, h, ms, us. To turn off any kind of rate limiting,
-        set either value to 0.
-
-        See <option>services.journald.rateLimitBurst</option> for important
-        considerations when setting this value.
-      '';
-    };
-
-    services.journald.rateLimitBurst = mkOption {
-      default = 10000;
-      type = types.int;
-      description = ''
-        Configures the rate limiting burst limit (number of messages per
-        interval) that is applied to all messages generated on the system.
-        This rate limiting is applied per-service, so that two services
-        which log do not interfere with each other's limit.
-
-        Note that the effective rate limit is multiplied by a factor derived
-        from the available free disk space for the journal as described on
-        <link xlink:href="https://www.freedesktop.org/software/systemd/man/journald.conf.html">
-        journald.conf(5)</link>.
-
-        Note that the total amount of logs stored is limited by journald settings
-        such as <literal>SystemMaxUse</literal>, which defaults to a 4 GB cap.
-
-        It is thus recommended to compute what period of time that you will be
-        able to store logs for when an application logs at full burst rate.
-        With default settings for log lines that are 100 Bytes long, this can
-        amount to just a few hours.
-      '';
-    };
-
-    services.journald.extraConfig = mkOption {
-      default = "";
-      type = types.lines;
-      example = "Storage=volatile";
-      description = ''
-        Extra config options for systemd-journald. See man journald.conf
-        for available options.
-      '';
-    };
-
-    services.journald.enableHttpGateway = mkOption {
-      default = false;
-      type = types.bool;
-      description = ''
-        Whether to enable the HTTP gateway to the journal.
-      '';
-    };
-
-    services.journald.forwardToSyslog = mkOption {
-      default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
-      defaultText = literalExpression "services.rsyslogd.enable || services.syslog-ng.enable";
-      type = types.bool;
-      description = ''
-        Whether to forward log messages to syslog.
-      '';
-    };
-
-    services.logind.extraConfig = mkOption {
-      default = "";
-      type = types.lines;
-      example = "IdleAction=lock";
-      description = ''
-        Extra config options for systemd-logind. See
-        <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html">
-        logind.conf(5)</link> for available options.
-      '';
-    };
-
-    services.logind.killUserProcesses = mkOption {
-      default = false;
-      type = types.bool;
-      description = ''
-        Specifies whether the processes of a user should be killed
-        when the user logs out.  If true, the scope unit corresponding
-        to the session and all processes inside that scope will be
-        terminated.  If false, the scope is "abandoned" (see
-        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.scope.html#">
-        systemd.scope(5)</link>), and processes are not killed.
-        </para>
-
-        <para>
-        See <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html#KillUserProcesses=">logind.conf(5)</link>
-        for more details.
-      '';
-    };
-
-    services.logind.lidSwitch = mkOption {
-      default = "suspend";
-      example = "ignore";
-      type = logindHandlerType;
-
-      description = ''
-        Specifies what to be done when the laptop lid is closed.
-      '';
-    };
-
-    services.logind.lidSwitchDocked = mkOption {
-      default = "ignore";
-      example = "suspend";
-      type = logindHandlerType;
-
-      description = ''
-        Specifies what to be done when the laptop lid is closed
-        and another screen is added.
-      '';
-    };
-
-    services.logind.lidSwitchExternalPower = mkOption {
-      default = config.services.logind.lidSwitch;
-      defaultText = literalExpression "services.logind.lidSwitch";
-      example = "ignore";
-      type = logindHandlerType;
-
-      description = ''
-        Specifies what to do when the laptop lid is closed and the system is
-        on external power. By default use the same action as specified in
-        services.logind.lidSwitch.
-      '';
-    };
-
     systemd.sleep.extraConfig = mkOption {
       default = "";
       type = types.lines;
@@ -742,95 +340,6 @@ in
       '';
     };
 
-    systemd.user.extraConfig = mkOption {
-      default = "";
-      type = types.lines;
-      example = "DefaultCPUAccounting=yes";
-      description = ''
-        Extra config options for systemd user instances. See man systemd-user.conf for
-        available options.
-      '';
-    };
-
-    systemd.tmpfiles.rules = mkOption {
-      type = types.listOf types.str;
-      default = [];
-      example = [ "d /tmp 1777 root root 10d" ];
-      description = ''
-        Rules for creation, deletion and cleaning of volatile and temporary files
-        automatically. See
-        <citerefentry><refentrytitle>tmpfiles.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
-        for the exact format.
-      '';
-    };
-
-    systemd.tmpfiles.packages = mkOption {
-      type = types.listOf types.package;
-      default = [];
-      example = literalExpression "[ pkgs.lvm2 ]";
-      apply = map getLib;
-      description = ''
-        List of packages containing <command>systemd-tmpfiles</command> rules.
-
-        All files ending in .conf found in
-        <filename><replaceable>pkg</replaceable>/lib/tmpfiles.d</filename>
-        will be included.
-        If this folder does not exist or does not contain any files an error will be returned instead.
-
-        If a <filename>lib</filename> output is available, rules are searched there and only there.
-        If there is no <filename>lib</filename> output it will fall back to <filename>out</filename>
-        and if that does not exist either, the default output will be used.
-      '';
-    };
-
-    systemd.user.units = mkOption {
-      description = "Definition of systemd per-user units.";
-      default = {};
-      type = with types; attrsOf (submodule (
-        { name, config, ... }:
-        { options = concreteUnitOptions;
-          config = {
-            unit = mkDefault (makeUnit name config);
-          };
-        }));
-    };
-
-    systemd.user.paths = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]);
-      description = "Definition of systemd per-user path units.";
-    };
-
-    systemd.user.services = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ] );
-      description = "Definition of systemd per-user service units.";
-    };
-
-    systemd.user.slices = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig ] );
-      description = "Definition of systemd per-user slice units.";
-    };
-
-    systemd.user.sockets = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ] );
-      description = "Definition of systemd per-user socket units.";
-    };
-
-    systemd.user.targets = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] );
-      description = "Definition of systemd per-user target units.";
-    };
-
-    systemd.user.timers = mkOption {
-      default = {};
-      type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ] );
-      description = "Definition of systemd per-user timer units.";
-    };
-
     systemd.additionalUpstreamSystemUnits = mkOption {
       default = [ ];
       type = types.listOf types.str;
@@ -845,10 +354,11 @@ in
       type = types.listOf types.str;
       example = [ "systemd-backlight@.service" ];
       description = ''
-        A list of units to suppress when generating system systemd configuration directory. This has
+        A list of units to skip when generating system systemd configuration directory. This has
         priority over upstream units, <option>systemd.units</option>, and
         <option>systemd.additionalUpstreamSystemUnits</option>. The main purpose of this is to
-        suppress a upstream systemd unit with any modifications made to it by other NixOS modules.
+        prevent a upstream systemd unit from being added to the initrd with any modifications made to it
+        by other NixOS modules.
       '';
     };
 
@@ -917,6 +427,9 @@ in
               (optional hasDeprecated
                 "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786."
               )
+              (optional (service.reloadIfChanged && service.reloadTriggers != [])
+                "Service '${name}.service' has both 'reloadIfChanged' and 'reloadTriggers' set. This is probably not what you want, because 'reloadTriggers' behave the same whay as 'restartTriggers' if 'reloadIfChanged' is set."
+              )
             ]
         )
         cfg.services
@@ -924,7 +437,7 @@ in
 
     system.build.units = cfg.units;
 
-    system.nssModules = [ systemd.out ];
+    system.nssModules = [ cfg.package.out ];
     system.nssDatabases = {
       hosts = (mkMerge [
         (mkOrder 400 ["mymachines"]) # 400 to ensure it comes before resolve (which is mkBefore'd)
@@ -938,7 +451,7 @@ in
       ]);
     };
 
-    environment.systemPackages = [ systemd ];
+    environment.systemPackages = [ cfg.package ];
 
     environment.etc = let
       # generate contents for /etc/systemd/system-${type} from attrset of links and packages
@@ -960,13 +473,18 @@ in
 
       enabledUpstreamSystemUnits = filter (n: ! elem n cfg.suppressedSystemUnits) upstreamSystemUnits;
       enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedSystemUnits) cfg.units;
-    in ({
-      "systemd/system".source = generateUnits "system" enabledUnits enabledUpstreamSystemUnits upstreamSystemWants;
 
-      "systemd/user".source = generateUnits "user" cfg.user.units upstreamUserUnits [];
+    in ({
+      "systemd/system".source = generateUnits {
+        type = "system";
+        units = enabledUnits;
+        upstreamUnits = enabledUpstreamSystemUnits;
+        upstreamWants = upstreamSystemWants;
+      };
 
       "systemd/system.conf".text = ''
         [Manager]
+        ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
         ${optionalString config.systemd.enableCgroupAccounting ''
           DefaultCPUAccounting=yes
           DefaultIOAccounting=yes
@@ -990,76 +508,17 @@ in
         ${config.systemd.extraConfig}
       '';
 
-      "systemd/user.conf".text = ''
-        [Manager]
-        ${config.systemd.user.extraConfig}
-      '';
-
-      "systemd/journald.conf".text = ''
-        [Journal]
-        Storage=persistent
-        RateLimitInterval=${config.services.journald.rateLimitInterval}
-        RateLimitBurst=${toString config.services.journald.rateLimitBurst}
-        ${optionalString (config.services.journald.console != "") ''
-          ForwardToConsole=yes
-          TTYPath=${config.services.journald.console}
-        ''}
-        ${optionalString (config.services.journald.forwardToSyslog) ''
-          ForwardToSyslog=yes
-        ''}
-        ${config.services.journald.extraConfig}
-      '';
-
-      "systemd/coredump.conf".text =
-        ''
-          [Coredump]
-          ${config.systemd.coredump.extraConfig}
-        '';
-
-      "systemd/logind.conf".text = ''
-        [Login]
-        KillUserProcesses=${if config.services.logind.killUserProcesses then "yes" else "no"}
-        HandleLidSwitch=${config.services.logind.lidSwitch}
-        HandleLidSwitchDocked=${config.services.logind.lidSwitchDocked}
-        HandleLidSwitchExternalPower=${config.services.logind.lidSwitchExternalPower}
-        ${config.services.logind.extraConfig}
-      '';
-
       "systemd/sleep.conf".text = ''
         [Sleep]
         ${config.systemd.sleep.extraConfig}
       '';
 
-      # install provided sysctl snippets
-      "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
-      "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
-
-      "tmpfiles.d".source = (pkgs.symlinkJoin {
-        name = "tmpfiles.d";
-        paths = map (p: p + "/lib/tmpfiles.d") cfg.tmpfiles.packages;
-        postBuild = ''
-          for i in $(cat $pathsPath); do
-            (test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
-              echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
-              exit 1
-            )
-          done
-        '' + concatMapStrings (name: optionalString (hasPrefix "tmpfiles.d/" name) ''
-          rm -f $out/${removePrefix "tmpfiles.d/" name}
-        '') config.system.build.etc.passthru.targets;
-      }) + "/*";
-
       "systemd/system-generators" = { source = hooks "generators" cfg.generators; };
       "systemd/system-shutdown" = { source = hooks "shutdown" cfg.shutdown; };
     });
 
     services.dbus.enable = true;
 
-    users.users.systemd-coredump = {
-      uid = config.ids.uids.systemd-coredump;
-      group = "systemd-coredump";
-    };
-    users.groups.systemd-coredump = {};
     users.users.systemd-network = {
       uid = config.ids.uids.systemd-network;
       group = "systemd-network";
@@ -1079,36 +538,6 @@ in
         unitConfig.X-StopOnReconfiguration = true;
       };
 
-    systemd.tmpfiles.packages = [
-      # Default tmpfiles rules provided by systemd
-      (pkgs.runCommand "systemd-default-tmpfiles" {} ''
-        mkdir -p $out/lib/tmpfiles.d
-        cd $out/lib/tmpfiles.d
-
-        ln -s "${systemd}/example/tmpfiles.d/home.conf"
-        ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
-        ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
-        ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
-        ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
-        ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
-        ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
-        ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
-        ln -s "${systemd}/example/tmpfiles.d/var.conf"
-        ln -s "${systemd}/example/tmpfiles.d/x11.conf"
-      '')
-      # User-specified tmpfiles rules
-      (pkgs.writeTextFile {
-        name = "nixos-tmpfiles.d";
-        destination = "/lib/tmpfiles.d/00-nixos.conf";
-        text = ''
-          # This file is created automatically and should not be modified.
-          # Please change the option ‘systemd.tmpfiles.rules’ instead.
-
-          ${concatStringsSep "\n" cfg.tmpfiles.rules}
-        '';
-      })
-    ];
-
     systemd.units =
          mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
       // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
@@ -1123,13 +552,16 @@ in
                    (v: let n = escapeSystemdPath v.where;
                        in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
 
-    systemd.user.units =
-         mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.user.paths
-      // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.user.services
-      // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.user.slices
-      // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.user.sockets
-      // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.user.targets
-      // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.user.timers;
+      # Environment of PID 1
+      systemd.managerEnvironment = {
+        # Doesn't contain systemd itself - everything works so it seems to use the compiled-in value for its tools
+        PATH = lib.makeBinPath config.system.fsPackages;
+        LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
+        TZDIR = "/etc/zoneinfo";
+        # If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable
+        SYSTEMD_UNIT_PATH = lib.mkIf (config.boot.extraSystemdUnitPaths != []) "${builtins.concatStringsSep ":" config.boot.extraSystemdUnitPaths}:";
+      };
+
 
     system.requiredKernelConfig = map config.lib.kernelConfig.isEnabled
       [ "DEVTMPFS" "CGROUPS" "INOTIFY_USER" "SIGNALFD" "TIMERFD" "EPOLL" "NET"
@@ -1138,11 +570,6 @@ in
         "TMPFS_XATTR" "SECCOMP"
       ];
 
-    users.groups.systemd-journal.gid = config.ids.gids.systemd-journal;
-    users.users.systemd-journal-gateway.uid = config.ids.uids.systemd-journal-gateway;
-    users.users.systemd-journal-gateway.group = "systemd-journal-gateway";
-    users.groups.systemd-journal-gateway.gid = config.ids.gids.systemd-journal-gateway;
-
     # Generate timer units for all services that have a ‘startAt’ value.
     systemd.timers =
       mapAttrs (name: service:
@@ -1151,50 +578,14 @@ in
         })
         (filterAttrs (name: service: service.enable && service.startAt != []) cfg.services);
 
-    # Generate timer units for all services that have a ‘startAt’ value.
-    systemd.user.timers =
-      mapAttrs (name: service:
-        { wantedBy = [ "timers.target" ];
-          timerConfig.OnCalendar = service.startAt;
-        })
-        (filterAttrs (name: service: service.startAt != []) cfg.user.services);
-
-    systemd.sockets.systemd-journal-gatewayd.wantedBy =
-      optional config.services.journald.enableHttpGateway "sockets.target";
-
-    # Provide the systemd-user PAM service, required to run systemd
-    # user instances.
-    security.pam.services.systemd-user =
-      { # Ensure that pam_systemd gets included. This is special-cased
-        # in systemd to provide XDG_RUNTIME_DIR.
-        startSession = true;
-      };
-
     # Some overrides to upstream units.
     systemd.services."systemd-backlight@".restartIfChanged = false;
     systemd.services."systemd-fsck@".restartIfChanged = false;
     systemd.services."systemd-fsck@".path = [ config.system.path ];
-    systemd.services."user@".restartIfChanged = false;
-    systemd.services.systemd-journal-flush.restartIfChanged = false;
     systemd.services.systemd-random-seed.restartIfChanged = false;
     systemd.services.systemd-remount-fs.restartIfChanged = false;
     systemd.services.systemd-update-utmp.restartIfChanged = false;
-    systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
     systemd.services.systemd-udev-settle.restartIfChanged = false; # Causes long delays in nixos-rebuild
-    # Restarting systemd-logind breaks X11
-    # - upstream commit: https://cgit.freedesktop.org/xorg/xserver/commit/?id=dc48bd653c7e101
-    # - systemd announcement: https://github.com/systemd/systemd/blob/22043e4317ecd2bc7834b48a6d364de76bb26d91/NEWS#L103-L112
-    # - this might be addressed in the future by xorg
-    #systemd.services.systemd-logind.restartTriggers = [ config.environment.etc."systemd/logind.conf".source ];
-    systemd.services.systemd-logind.restartIfChanged = false;
-    systemd.services.systemd-logind.stopIfChanged = false;
-    # The user-runtime-dir@ service is managed by systemd-logind we should not touch it or else we break the users' sessions.
-    systemd.services."user-runtime-dir@".stopIfChanged = false;
-    systemd.services."user-runtime-dir@".restartIfChanged = false;
-    systemd.services.systemd-journald.restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
-    systemd.services.systemd-journald.stopIfChanged = false;
-    systemd.services."systemd-journald@".restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
-    systemd.services."systemd-journald@".stopIfChanged = false;
     systemd.targets.local-fs.unitConfig.X-StopOnReconfiguration = true;
     systemd.targets.remote-fs.unitConfig.X-StopOnReconfiguration = true;
     systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
@@ -1205,13 +596,26 @@ in
     systemd.services.systemd-remount-fs.unitConfig.ConditionVirtualization = "!container";
     systemd.services.systemd-random-seed.unitConfig.ConditionVirtualization = "!container";
 
-    boot.kernel.sysctl."kernel.core_pattern" = mkIf (!cfg.coredump.enable) "core";
-
     # Increase numeric PID range (set directly instead of copying a one-line file from systemd)
     # https://github.com/systemd/systemd/pull/12226
     boot.kernel.sysctl."kernel.pid_max" = mkIf pkgs.stdenv.is64bit (lib.mkDefault 4194304);
 
     boot.kernelParams = optional (!cfg.enableUnifiedCgroupHierarchy) "systemd.unified_cgroup_hierarchy=0";
+
+    services.logrotate.settings = {
+      "/var/log/btmp" = mapAttrs (_: mkDefault) {
+        frequency = "monthly";
+        rotate = 1;
+        create = "0660 root ${config.users.groups.utmp.name}";
+        minsize = "1M";
+      };
+      "/var/log/wtmp" = mapAttrs (_: mkDefault) {
+        frequency = "monthly";
+        rotate = 1;
+        create = "0664 root ${config.users.groups.utmp.name}";
+        minsize = "1M";
+      };
+    };
   };
 
   # FIXME: Remove these eventually.
diff --git a/nixos/modules/system/boot/systemd/coredump.nix b/nixos/modules/system/boot/systemd/coredump.nix
new file mode 100644
index 0000000000000..b6ee2cff1f9af
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/coredump.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.systemd.coredump;
+  systemd = config.systemd.package;
+in {
+  options = {
+    systemd.coredump.enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether core dumps should be processed by
+        <command>systemd-coredump</command>. If disabled, core dumps
+        appear in the current directory of the crashing process.
+      '';
+    };
+
+    systemd.coredump.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "Storage=journal";
+      description = ''
+        Extra config options for systemd-coredump. See coredump.conf(5) man page
+        for available options.
+      '';
+    };
+  };
+
+  config = {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-coredump.socket"
+      "systemd-coredump@.service"
+    ];
+
+    environment.etc = {
+      "systemd/coredump.conf".text =
+      ''
+        [Coredump]
+        ${cfg.extraConfig}
+      '';
+
+      # install provided sysctl snippets
+      "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
+      "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
+    };
+
+    users.users.systemd-coredump = {
+      uid = config.ids.uids.systemd-coredump;
+      group = "systemd-coredump";
+    };
+    users.groups.systemd-coredump = {};
+
+    boot.kernel.sysctl."kernel.core_pattern" = mkIf (!cfg.enable) "core";
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/initrd-secrets.nix b/nixos/modules/system/boot/systemd/initrd-secrets.nix
new file mode 100644
index 0000000000000..bc65880719d7a
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/initrd-secrets.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, lib, ... }:
+
+{
+  config = lib.mkIf (config.boot.initrd.enable && config.boot.initrd.systemd.enable) {
+    # Copy secrets into the initrd if they cannot be appended
+    boot.initrd.systemd.contents = lib.mkIf (!config.boot.loader.supportsInitrdSecrets)
+      (lib.mapAttrs' (dest: source: lib.nameValuePair "/.initrd-secrets/${dest}" { source = if source == null then dest else source; }) config.boot.initrd.secrets);
+
+    # Copy secrets to their respective locations
+    boot.initrd.systemd.services.initrd-nixos-copy-secrets = lib.mkIf (config.boot.initrd.secrets != {}) {
+      description = "Copy secrets into place";
+      # Run as early as possible
+      wantedBy = [ "sysinit.target" ];
+      before = [ "cryptsetup-pre.target" ];
+      unitConfig.DefaultDependencies = false;
+
+      # We write the secrets to /.initrd-secrets and move them because this allows
+      # secrets to be written to /run. If we put the secret directly to /run and
+      # drop this service, we'd mount the /run tmpfs over the secret, making it
+      # invisible in stage 2.
+      script = ''
+        for secret in $(cd /.initrd-secrets; find . -type f); do
+          mkdir -p "$(dirname "/$secret")"
+          cp "/.initrd-secrets/$secret" "/$secret"
+        done
+      '';
+
+      unitConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+    # The script needs this
+    boot.initrd.systemd.extraBin.find = "${pkgs.findutils}/bin/find";
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix
new file mode 100644
index 0000000000000..5f93a8ac3c734
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -0,0 +1,502 @@
+{ lib, config, utils, pkgs, ... }:
+
+with lib;
+
+let
+  inherit (utils) systemdUtils escapeSystemdPath;
+  inherit (systemdUtils.lib)
+    generateUnits
+    pathToUnit
+    serviceToUnit
+    sliceToUnit
+    socketToUnit
+    targetToUnit
+    timerToUnit
+    mountToUnit
+    automountToUnit;
+
+
+  cfg = config.boot.initrd.systemd;
+
+  # Copied from fedora
+  upstreamUnits = [
+    "basic.target"
+    "ctrl-alt-del.target"
+    "emergency.service"
+    "emergency.target"
+    "final.target"
+    "halt.target"
+    "initrd-cleanup.service"
+    "initrd-fs.target"
+    "initrd-parse-etc.service"
+    "initrd-root-device.target"
+    "initrd-root-fs.target"
+    "initrd-switch-root.service"
+    "initrd-switch-root.target"
+    "initrd.target"
+    "kexec.target"
+    "kmod-static-nodes.service"
+    "local-fs-pre.target"
+    "local-fs.target"
+    "multi-user.target"
+    "paths.target"
+    "poweroff.target"
+    "reboot.target"
+    "rescue.service"
+    "rescue.target"
+    "rpcbind.target"
+    "shutdown.target"
+    "sigpwr.target"
+    "slices.target"
+    "sockets.target"
+    "swap.target"
+    "sysinit.target"
+    "sys-kernel-config.mount"
+    "syslog.socket"
+    "systemd-ask-password-console.path"
+    "systemd-ask-password-console.service"
+    "systemd-fsck@.service"
+    "systemd-halt.service"
+    "systemd-hibernate-resume@.service"
+    "systemd-journald-audit.socket"
+    "systemd-journald-dev-log.socket"
+    "systemd-journald.service"
+    "systemd-journald.socket"
+    "systemd-kexec.service"
+    "systemd-modules-load.service"
+    "systemd-poweroff.service"
+    "systemd-reboot.service"
+    "systemd-sysctl.service"
+    "systemd-tmpfiles-setup-dev.service"
+    "systemd-tmpfiles-setup.service"
+    "timers.target"
+    "umount.target"
+
+    # TODO: Networking
+    # "network-online.target"
+    # "network-pre.target"
+    # "network.target"
+    # "nss-lookup.target"
+    # "nss-user-lookup.target"
+    # "remote-fs-pre.target"
+    # "remote-fs.target"
+  ] ++ cfg.additionalUpstreamUnits;
+
+  upstreamWants = [
+    "sysinit.target.wants"
+  ];
+
+  enabledUpstreamUnits = filter (n: ! elem n cfg.suppressedUnits) upstreamUnits;
+  enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedUnits) cfg.units;
+  jobScripts = concatLists (mapAttrsToList (_: unit: unit.jobScripts or []) (filterAttrs (_: v: v.enable) cfg.services));
+
+  stage1Units = generateUnits {
+    type = "initrd";
+    units = enabledUnits;
+    upstreamUnits = enabledUpstreamUnits;
+    inherit upstreamWants;
+    inherit (cfg) packages package;
+  };
+
+  fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
+
+  fstab = pkgs.writeText "initrd-fstab" (lib.concatMapStringsSep "\n"
+    ({ fsType, mountPoint, device, options, autoFormat, autoResize, ... }@fs: let
+        opts = options ++ optional autoFormat "x-systemd.makefs" ++ optional autoResize "x-systemd.growfs";
+      in "${device} /sysroot${mountPoint} ${fsType} ${lib.concatStringsSep "," opts}") fileSystems);
+
+  needMakefs = lib.any (fs: fs.autoFormat) fileSystems;
+  needGrowfs = lib.any (fs: fs.autoResize) fileSystems;
+
+  kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
+  modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; };
+  firmware = config.hardware.firmware;
+  # Determine the set of modules that we need to mount the root FS.
+  modulesClosure = pkgs.makeModulesClosure {
+    rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
+    kernel = modulesTree;
+    firmware = firmware;
+    allowMissing = false;
+  };
+
+  initrdBinEnv = pkgs.buildEnv {
+    name = "initrd-bin-env";
+    paths = map getBin cfg.initrdBin;
+    pathsToLink = ["/bin" "/sbin"];
+    postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -s '${v}' $out/bin/'${n}'") cfg.extraBin);
+  };
+
+  initialRamdisk = pkgs.makeInitrdNG {
+    name = "initrd-${kernel-name}";
+    inherit (config.boot.initrd) compressor compressorArgs prepend;
+
+    contents = map (path: { object = path; symlink = ""; }) (subtractLists cfg.suppressedStorePaths cfg.storePaths)
+      ++ mapAttrsToList (_: v: { object = v.source; symlink = v.target; }) (filterAttrs (_: v: v.enable) cfg.contents);
+  };
+
+in {
+  options.boot.initrd.systemd = {
+    enable = mkEnableOption ''systemd in initrd.
+
+      Note: This is in very early development and is highly
+      experimental. Most of the features NixOS supports in initrd are
+      not yet supported by the intrd generated with this option.
+    '';
+
+    package = (mkPackageOption pkgs "systemd" {
+      default = "systemdStage1";
+    }) // {
+      visible = false;
+    };
+
+    contents = mkOption {
+      description = "Set of files that have to be linked into the initrd";
+      example = literalExpression ''
+        {
+          "/etc/hostname".text = "mymachine";
+        }
+      '';
+      visible = false;
+      default = {};
+      type = utils.systemdUtils.types.initrdContents;
+    };
+
+    storePaths = mkOption {
+      description = ''
+        Store paths to copy into the initrd as well.
+      '';
+      type = with types; listOf (oneOf [ singleLineStr package ]);
+      default = [];
+    };
+
+    extraBin = mkOption {
+      description = ''
+        Tools to add to /bin
+      '';
+      example = literalExpression ''
+        {
+          umount = ''${pkgs.util-linux}/bin/umount;
+        }
+      '';
+      type = types.attrsOf types.path;
+      default = {};
+    };
+
+    suppressedStorePaths = mkOption {
+      description = ''
+        Store paths specified in the storePaths option that
+        should not be copied.
+      '';
+      type = types.listOf types.singleLineStr;
+      default = [];
+    };
+
+    emergencyAccess = mkOption {
+      type = with types; oneOf [ bool (nullOr (passwdEntry str)) ];
+      visible = false;
+      description = ''
+        Set to true for unauthenticated emergency access, and false for
+        no emergency access.
+
+        Can also be set to a hashed super user password to allow
+        authenticated access to the emergency mode.
+      '';
+      default = false;
+    };
+
+    initrdBin = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      visible = false;
+      description = ''
+        Packages to include in /bin for the stage 1 emergency shell.
+      '';
+    };
+
+    additionalUpstreamUnits = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      visible = false;
+      example = [ "debug-shell.service" "systemd-quotacheck.service" ];
+      description = ''
+        Additional units shipped with systemd that shall be enabled.
+      '';
+    };
+
+    suppressedUnits = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "systemd-backlight@.service" ];
+      visible = false;
+      description = ''
+        A list of units to skip when generating system systemd configuration directory. This has
+        priority over upstream units, <option>boot.initrd.systemd.units</option>, and
+        <option>boot.initrd.systemd.additionalUpstreamUnits</option>. The main purpose of this is to
+        prevent a upstream systemd unit from being added to the initrd with any modifications made to it
+        by other NixOS modules.
+      '';
+    };
+
+    units = mkOption {
+      description = "Definition of systemd units.";
+      default = {};
+      visible = false;
+      type = systemdUtils.types.units;
+    };
+
+    packages = mkOption {
+      default = [];
+      visible = false;
+      type = types.listOf types.package;
+      example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
+      description = "Packages providing systemd units and hooks.";
+    };
+
+    targets = mkOption {
+      default = {};
+      visible = false;
+      type = systemdUtils.types.initrdTargets;
+      description = "Definition of systemd target units.";
+    };
+
+    services = mkOption {
+      default = {};
+      type = systemdUtils.types.initrdServices;
+      visible = false;
+      description = "Definition of systemd service units.";
+    };
+
+    sockets = mkOption {
+      default = {};
+      type = systemdUtils.types.initrdSockets;
+      visible = false;
+      description = "Definition of systemd socket units.";
+    };
+
+    timers = mkOption {
+      default = {};
+      type = systemdUtils.types.initrdTimers;
+      visible = false;
+      description = "Definition of systemd timer units.";
+    };
+
+    paths = mkOption {
+      default = {};
+      type = systemdUtils.types.initrdPaths;
+      visible = false;
+      description = "Definition of systemd path units.";
+    };
+
+    mounts = mkOption {
+      default = [];
+      type = systemdUtils.types.initrdMounts;
+      visible = false;
+      description = ''
+        Definition of systemd mount units.
+        This is a list instead of an attrSet, because systemd mandates the names to be derived from
+        the 'where' attribute.
+      '';
+    };
+
+    automounts = mkOption {
+      default = [];
+      type = systemdUtils.types.automounts;
+      visible = false;
+      description = ''
+        Definition of systemd automount units.
+        This is a list instead of an attrSet, because systemd mandates the names to be derived from
+        the 'where' attribute.
+      '';
+    };
+
+    slices = mkOption {
+      default = {};
+      type = systemdUtils.types.slices;
+      visible = false;
+      description = "Definition of slice configurations.";
+    };
+  };
+
+  config = mkIf (config.boot.initrd.enable && cfg.enable) {
+    system.build = { inherit initialRamdisk; };
+
+    boot.initrd.availableKernelModules = [ "autofs4" ]; # systemd needs this for some features
+
+    boot.initrd.systemd = {
+      initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
+      extraBin = {
+        less = "${pkgs.less}/bin/less";
+        mount = "${cfg.package.util-linux}/bin/mount";
+        umount = "${cfg.package.util-linux}/bin/umount";
+      };
+
+      contents = {
+        "/init".source = "${cfg.package}/lib/systemd/systemd";
+        "/etc/systemd/system".source = stage1Units;
+
+        "/etc/systemd/system.conf".text = ''
+          [Manager]
+          DefaultEnvironment=PATH=/bin:/sbin ${optionalString (isBool cfg.emergencyAccess && cfg.emergencyAccess) "SYSTEMD_SULOGIN_FORCE=1"}
+        '';
+
+        "/etc/fstab".source = fstab;
+
+        "/lib/modules".source = "${modulesClosure}/lib/modules";
+        "/lib/firmware".source = "${modulesClosure}/lib/firmware";
+
+        "/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
+
+        "/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd";
+        "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::";
+
+        "/bin".source = "${initrdBinEnv}/bin";
+        "/sbin".source = "${initrdBinEnv}/sbin";
+
+        "/etc/sysctl.d/nixos.conf".text = "kernel.modprobe = /sbin/modprobe";
+        "/etc/modprobe.d/systemd.conf".source = "${cfg.package}/lib/modprobe.d/systemd.conf";
+        "/etc/modprobe.d/ubuntu.conf".source = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" { } ''
+          ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
+        '';
+        "/etc/modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
+
+      };
+
+      storePaths = [
+        # systemd tooling
+        "${cfg.package}/lib/systemd/systemd-fsck"
+        (lib.mkIf needGrowfs "${cfg.package}/lib/systemd/systemd-growfs")
+        "${cfg.package}/lib/systemd/systemd-hibernate-resume"
+        "${cfg.package}/lib/systemd/systemd-journald"
+        (lib.mkIf needMakefs "${cfg.package}/lib/systemd/systemd-makefs")
+        "${cfg.package}/lib/systemd/systemd-modules-load"
+        "${cfg.package}/lib/systemd/systemd-remount-fs"
+        "${cfg.package}/lib/systemd/systemd-shutdown"
+        "${cfg.package}/lib/systemd/systemd-sulogin-shell"
+        "${cfg.package}/lib/systemd/systemd-sysctl"
+
+        # generators
+        "${cfg.package}/lib/systemd/system-generators/systemd-debug-generator"
+        "${cfg.package}/lib/systemd/system-generators/systemd-fstab-generator"
+        "${cfg.package}/lib/systemd/system-generators/systemd-gpt-auto-generator"
+        "${cfg.package}/lib/systemd/system-generators/systemd-hibernate-resume-generator"
+        "${cfg.package}/lib/systemd/system-generators/systemd-run-generator"
+
+        # utilities needed by systemd
+        "${cfg.package.util-linux}/bin/mount"
+        "${cfg.package.util-linux}/bin/umount"
+        "${cfg.package.util-linux}/bin/sulogin"
+
+        # so NSS can look up usernames
+        "${pkgs.glibc}/lib/libnss_files.so.2"
+      ] ++ jobScripts;
+
+      targets.initrd.aliases = ["default.target"];
+      units =
+           mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
+        // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
+        // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.slices
+        // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.sockets
+        // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.targets
+        // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.timers
+        // listToAttrs (map
+                     (v: let n = escapeSystemdPath v.where;
+                         in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts)
+        // listToAttrs (map
+                     (v: let n = escapeSystemdPath v.where;
+                         in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
+
+      # The unit in /run/systemd/generator shadows the unit in
+      # /etc/systemd/system, but will still apply drop-ins from
+      # /etc/systemd/system/foo.service.d/
+      #
+      # We need IgnoreOnIsolate, otherwise the Requires dependency of
+      # a mount unit on its makefs unit causes it to be unmounted when
+      # we isolate for switch-root. Use a dummy package so that
+      # generateUnits will generate drop-ins instead of unit files.
+      packages = [(pkgs.runCommand "dummy" {} ''
+        mkdir -p $out/etc/systemd/system
+        touch $out/etc/systemd/system/systemd-{makefs,growfs}@.service
+      '')];
+      services."systemd-makefs@" = lib.mkIf needMakefs { unitConfig.IgnoreOnIsolate = true; };
+      services."systemd-growfs@" = lib.mkIf needGrowfs { unitConfig.IgnoreOnIsolate = true; };
+
+      services.initrd-nixos-activation = {
+        after = [ "initrd-fs.target" ];
+        requiredBy = [ "initrd.target" ];
+        unitConfig.AssertPathExists = "/etc/initrd-release";
+        serviceConfig.Type = "oneshot";
+        description = "NixOS Activation";
+
+        script = /* bash */ ''
+          set -uo pipefail
+          export PATH="/bin:${cfg.package.util-linux}/bin"
+
+          # Figure out what closure to boot
+          closure=
+          for o in $(< /proc/cmdline); do
+              case $o in
+                  init=*)
+                      IFS== read -r -a initParam <<< "$o"
+                      closure="$(dirname "''${initParam[1]}")"
+                      ;;
+              esac
+          done
+
+          # Sanity check
+          if [ -z "''${closure:-}" ]; then
+            echo 'No init= parameter on the kernel command line' >&2
+            exit 1
+          fi
+
+          # If we are not booting a NixOS closure (e.g. init=/bin/sh),
+          # we don't know what root to prepare so we don't do anything
+          if ! [ -x "/sysroot$closure/prepare-root" ]; then
+            echo "NEW_INIT=''${initParam[1]}" > /etc/switch-root.conf
+            echo "$closure does not look like a NixOS installation - not activating"
+            exit 0
+          fi
+          echo 'NEW_INIT=' > /etc/switch-root.conf
+
+
+          # We need to propagate /run for things like /run/booted-system
+          # and /run/current-system.
+          mkdir -p /sysroot/run
+          mount --bind /run /sysroot/run
+
+          # Initialize the system
+          export IN_NIXOS_SYSTEMD_STAGE1=true
+          exec chroot /sysroot $closure/prepare-root
+        '';
+      };
+
+      # This will either call systemctl with the new init as the last parameter (which
+      # is the case when not booting a NixOS system) or with an empty string, causing
+      # systemd to bypass its verification code that checks whether the next file is a systemd
+      # and using its compiled-in value
+      services.initrd-switch-root.serviceConfig = {
+        EnvironmentFile = "-/etc/switch-root.conf";
+        ExecStart = [
+          ""
+          ''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
+        ];
+      };
+
+      services.panic-on-fail = {
+        wantedBy = ["emergency.target"];
+        unitConfig = {
+          DefaultDependencies = false;
+          ConditionKernelCommandLine = [
+            "|boot.panic_on_fail"
+            "|stage1panic"
+          ];
+        };
+        script = ''
+          echo c > /proc/sysrq-trigger
+        '';
+        serviceConfig.Type = "oneshot";
+      };
+    };
+
+    boot.kernelParams = lib.mkIf (config.boot.resumeDevice != "") [ "resume=${config.boot.resumeDevice}" ];
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/journald.nix b/nixos/modules/system/boot/systemd/journald.nix
new file mode 100644
index 0000000000000..7e14c8ae4077f
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/journald.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.journald;
+in {
+  options = {
+    services.journald.console = mkOption {
+      default = "";
+      type = types.str;
+      description = "If non-empty, write log messages to the specified TTY device.";
+    };
+
+    services.journald.rateLimitInterval = mkOption {
+      default = "30s";
+      type = types.str;
+      description = ''
+        Configures the rate limiting interval that is applied to all
+        messages generated on the system. This rate limiting is applied
+        per-service, so that two services which log do not interfere with
+        each other's limit. The value may be specified in the following
+        units: s, min, h, ms, us. To turn off any kind of rate limiting,
+        set either value to 0.
+
+        See <option>services.journald.rateLimitBurst</option> for important
+        considerations when setting this value.
+      '';
+    };
+
+    services.journald.rateLimitBurst = mkOption {
+      default = 10000;
+      type = types.int;
+      description = ''
+        Configures the rate limiting burst limit (number of messages per
+        interval) that is applied to all messages generated on the system.
+        This rate limiting is applied per-service, so that two services
+        which log do not interfere with each other's limit.
+
+        Note that the effective rate limit is multiplied by a factor derived
+        from the available free disk space for the journal as described on
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/journald.conf.html">
+        journald.conf(5)</link>.
+
+        Note that the total amount of logs stored is limited by journald settings
+        such as <literal>SystemMaxUse</literal>, which defaults to a 4 GB cap.
+
+        It is thus recommended to compute what period of time that you will be
+        able to store logs for when an application logs at full burst rate.
+        With default settings for log lines that are 100 Bytes long, this can
+        amount to just a few hours.
+      '';
+    };
+
+    services.journald.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "Storage=volatile";
+      description = ''
+        Extra config options for systemd-journald. See man journald.conf
+        for available options.
+      '';
+    };
+
+    services.journald.enableHttpGateway = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable the HTTP gateway to the journal.
+      '';
+    };
+
+    services.journald.forwardToSyslog = mkOption {
+      default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
+      defaultText = literalExpression "services.rsyslogd.enable || services.syslog-ng.enable";
+      type = types.bool;
+      description = ''
+        Whether to forward log messages to syslog.
+      '';
+    };
+  };
+
+  config = {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-journald.socket"
+      "systemd-journald@.socket"
+      "systemd-journald-varlink@.socket"
+      "systemd-journald.service"
+      "systemd-journald@.service"
+      "systemd-journal-flush.service"
+      "systemd-journal-catalog-update.service"
+      ] ++ (optional (!config.boot.isContainer) "systemd-journald-audit.socket") ++ [
+      "systemd-journald-dev-log.socket"
+      "syslog.socket"
+      ] ++ optionals cfg.enableHttpGateway [
+      "systemd-journal-gatewayd.socket"
+      "systemd-journal-gatewayd.service"
+      ];
+
+    environment.etc = {
+      "systemd/journald.conf".text = ''
+        [Journal]
+        Storage=persistent
+        RateLimitInterval=${cfg.rateLimitInterval}
+        RateLimitBurst=${toString cfg.rateLimitBurst}
+        ${optionalString (cfg.console != "") ''
+          ForwardToConsole=yes
+          TTYPath=${cfg.console}
+        ''}
+        ${optionalString (cfg.forwardToSyslog) ''
+          ForwardToSyslog=yes
+        ''}
+        ${cfg.extraConfig}
+      '';
+    };
+
+    users.groups.systemd-journal.gid = config.ids.gids.systemd-journal;
+    users.users.systemd-journal-gateway.uid = config.ids.uids.systemd-journal-gateway;
+    users.users.systemd-journal-gateway.group = "systemd-journal-gateway";
+    users.groups.systemd-journal-gateway.gid = config.ids.gids.systemd-journal-gateway;
+
+    systemd.sockets.systemd-journal-gatewayd.wantedBy =
+      optional cfg.enableHttpGateway "sockets.target";
+
+    systemd.services.systemd-journal-flush.restartIfChanged = false;
+    systemd.services.systemd-journald.restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
+    systemd.services.systemd-journald.stopIfChanged = false;
+    systemd.services."systemd-journald@".restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
+    systemd.services."systemd-journald@".stopIfChanged = false;
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/logind.nix b/nixos/modules/system/boot/systemd/logind.nix
new file mode 100644
index 0000000000000..97ac588bce174
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/logind.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logind;
+
+  logindHandlerType = types.enum [
+    "ignore" "poweroff" "reboot" "halt" "kexec" "suspend"
+    "hibernate" "hybrid-sleep" "suspend-then-hibernate" "lock"
+  ];
+in
+{
+  options = {
+    services.logind.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "IdleAction=lock";
+      description = ''
+        Extra config options for systemd-logind. See
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html">
+        logind.conf(5)</link> for available options.
+      '';
+    };
+
+    services.logind.killUserProcesses = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Specifies whether the processes of a user should be killed
+        when the user logs out.  If true, the scope unit corresponding
+        to the session and all processes inside that scope will be
+        terminated.  If false, the scope is "abandoned" (see
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.scope.html#">
+        systemd.scope(5)</link>), and processes are not killed.
+        </para>
+
+        <para>
+        See <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html#KillUserProcesses=">logind.conf(5)</link>
+        for more details.
+      '';
+    };
+
+    services.logind.lidSwitch = mkOption {
+      default = "suspend";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to be done when the laptop lid is closed.
+      '';
+    };
+
+    services.logind.lidSwitchDocked = mkOption {
+      default = "ignore";
+      example = "suspend";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to be done when the laptop lid is closed
+        and another screen is added.
+      '';
+    };
+
+    services.logind.lidSwitchExternalPower = mkOption {
+      default = cfg.lidSwitch;
+      defaultText = literalExpression "services.logind.lidSwitch";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to do when the laptop lid is closed and the system is
+        on external power. By default use the same action as specified in
+        services.logind.lidSwitch.
+      '';
+    };
+  };
+
+  config = {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-logind.service"
+      "autovt@.service"
+      "systemd-user-sessions.service"
+    ] ++ optionals config.systemd.package.withImportd [
+      "dbus-org.freedesktop.import1.service"
+    ] ++ optionals config.systemd.package.withMachined [
+      "dbus-org.freedesktop.machine1.service"
+    ] ++ [
+      "dbus-org.freedesktop.login1.service"
+      "user@.service"
+      "user-runtime-dir@.service"
+    ];
+
+    environment.etc = {
+      "systemd/logind.conf".text = ''
+        [Login]
+        KillUserProcesses=${if cfg.killUserProcesses then "yes" else "no"}
+        HandleLidSwitch=${cfg.lidSwitch}
+        HandleLidSwitchDocked=${cfg.lidSwitchDocked}
+        HandleLidSwitchExternalPower=${cfg.lidSwitchExternalPower}
+        ${cfg.extraConfig}
+      '';
+    };
+
+    # Restarting systemd-logind breaks X11
+    # - upstream commit: https://cgit.freedesktop.org/xorg/xserver/commit/?id=dc48bd653c7e101
+    # - systemd announcement: https://github.com/systemd/systemd/blob/22043e4317ecd2bc7834b48a6d364de76bb26d91/NEWS#L103-L112
+    # - this might be addressed in the future by xorg
+    #systemd.services.systemd-logind.restartTriggers = [ config.environment.etc."systemd/logind.conf".source ];
+    systemd.services.systemd-logind.restartIfChanged = false;
+    systemd.services.systemd-logind.stopIfChanged = false;
+
+    # The user-runtime-dir@ service is managed by systemd-logind we should not touch it or else we break the users' sessions.
+    systemd.services."user-runtime-dir@".stopIfChanged = false;
+    systemd.services."user-runtime-dir@".restartIfChanged = false;
+  };
+}
diff --git a/nixos/modules/system/boot/systemd-nspawn.nix b/nixos/modules/system/boot/systemd/nspawn.nix
index 02d2660add897..da03c60db5281 100644
--- a/nixos/modules/system/boot/systemd-nspawn.nix
+++ b/nixos/modules/system/boot/systemd/nspawn.nix
@@ -16,7 +16,7 @@ let
       "LimitNOFILE" "LimitAS" "LimitNPROC" "LimitMEMLOCK" "LimitLOCKS"
       "LimitSIGPENDING" "LimitMSGQUEUE" "LimitNICE" "LimitRTPRIO" "LimitRTTIME"
       "OOMScoreAdjust" "CPUAffinity" "Hostname" "ResolvConf" "Timezone"
-      "LinkJournal"
+      "LinkJournal" "Ephemeral" "AmbientCapability"
     ])
     (assertValueOneOf "Boot" boolValues)
     (assertValueOneOf "ProcessTwo" boolValues)
@@ -26,11 +26,13 @@ let
   checkFiles = checkUnitConfig "Files" [
     (assertOnlyFields [
       "ReadOnly" "Volatile" "Bind" "BindReadOnly" "TemporaryFileSystem"
-      "Overlay" "OverlayReadOnly" "PrivateUsersChown"
+      "Overlay" "OverlayReadOnly" "PrivateUsersChown" "BindUser"
+      "Inaccessible" "PrivateUserOwnership"
     ])
     (assertValueOneOf "ReadOnly" boolValues)
     (assertValueOneOf "Volatile" (boolValues ++ [ "state" ]))
     (assertValueOneOf "PrivateUsersChown" boolValues)
+    (assertValueOneOf "PrivateUserOwnership" [ "off" "chown" "map" "auto" ])
   ];
 
   checkNetwork = checkUnitConfig "Network" [
@@ -116,18 +118,16 @@ in {
     in
       mkMerge [
         (mkIf (cfg != {}) {
-          environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits' false "nspawn" units [] []);
+          environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits {
+            allowCollisions = false;
+            type = "nspawn";
+            inherit units;
+            upstreamUnits = [];
+            upstreamWants = [];
+          });
         })
         {
           systemd.targets.multi-user.wants = [ "machines.target" ];
-
-          # Workaround for https://github.com/NixOS/nixpkgs/pull/67232#issuecomment-531315437 and https://github.com/systemd/systemd/issues/13622
-          # Once systemd fixes this upstream, we can re-enable -U
-          systemd.services."systemd-nspawn@".serviceConfig.ExecStart = [
-            ""  # deliberately empty. signals systemd to override the ExecStart
-            # Only difference between upstream is that we do not pass the -U flag
-            "${config.systemd.package}/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth --settings=override --machine=%i"
-          ];
         }
       ];
 }
diff --git a/nixos/modules/system/boot/systemd/shutdown.nix b/nixos/modules/system/boot/systemd/shutdown.nix
new file mode 100644
index 0000000000000..ca4cdf827d953
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/shutdown.nix
@@ -0,0 +1,58 @@
+{ config, lib, utils, pkgs, ... }: let
+
+  cfg = config.systemd.shutdownRamfs;
+
+  ramfsContents = let
+    storePaths = map (p: "${p}\n") cfg.storePaths;
+    contents = lib.mapAttrsToList (_: v: "${v.source}\n${v.target}") (lib.filterAttrs (_: v: v.enable) cfg.contents);
+  in pkgs.writeText "shutdown-ramfs-contents" (lib.concatStringsSep "\n" (storePaths ++ contents));
+
+in {
+  options.systemd.shutdownRamfs = {
+    enable = lib.mkEnableOption "pivoting back to an initramfs for shutdown" // { default = true; };
+    contents = lib.mkOption {
+      description = "Set of files that have to be linked into the shutdown ramfs";
+      example = lib.literalExpression ''
+        {
+          "/lib/systemd/system-shutdown/zpool-sync-shutdown".source = writeShellScript "zpool" "exec ''${zfs}/bin/zpool sync"
+        }
+      '';
+      type = utils.systemdUtils.types.initrdContents;
+    };
+
+    storePaths = lib.mkOption {
+      description = ''
+        Store paths to copy into the shutdown ramfs as well.
+      '';
+      type = lib.types.listOf lib.types.singleLineStr;
+      default = [];
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.shutdownRamfs.contents."/shutdown".source = "${config.systemd.package}/lib/systemd/systemd-shutdown";
+    systemd.shutdownRamfs.storePaths = [pkgs.runtimeShell "${pkgs.coreutils}/bin"];
+
+    systemd.services.generate-shutdown-ramfs = {
+      description = "Generate shutdown ramfs";
+      wantedBy = [ "shutdown.target" ];
+      before = [ "shutdown.target" ];
+      unitConfig = {
+        DefaultDependencies = false;
+        ConditionFileIsExecutable = [
+          "!/run/initramfs/shutdown"
+        ];
+      };
+
+      path = [pkgs.util-linux pkgs.makeInitrdNGTool];
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p /run/initramfs
+        if ! mountpoint -q /run/initramfs; then
+          mount -t tmpfs tmpfs /run/initramfs
+        fi
+        make-initrd-ng ${ramfsContents} /run/initramfs
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/tmpfiles.nix b/nixos/modules/system/boot/systemd/tmpfiles.nix
new file mode 100644
index 0000000000000..97d60e9d6527b
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/tmpfiles.nix
@@ -0,0 +1,120 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.systemd.tmpfiles;
+  systemd = config.systemd.package;
+in
+{
+  options = {
+    systemd.tmpfiles.rules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "d /tmp 1777 root root 10d" ];
+      description = ''
+        Rules for creation, deletion and cleaning of volatile and temporary files
+        automatically. See
+        <citerefentry><refentrytitle>tmpfiles.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        for the exact format.
+      '';
+    };
+
+    systemd.tmpfiles.packages = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ pkgs.lvm2 ]";
+      apply = map getLib;
+      description = ''
+        List of packages containing <command>systemd-tmpfiles</command> rules.
+
+        All files ending in .conf found in
+        <filename><replaceable>pkg</replaceable>/lib/tmpfiles.d</filename>
+        will be included.
+        If this folder does not exist or does not contain any files an error will be returned instead.
+
+        If a <filename>lib</filename> output is available, rules are searched there and only there.
+        If there is no <filename>lib</filename> output it will fall back to <filename>out</filename>
+        and if that does not exist either, the default output will be used.
+      '';
+    };
+  };
+
+  config = {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-tmpfiles-clean.service"
+      "systemd-tmpfiles-clean.timer"
+      "systemd-tmpfiles-setup.service"
+      "systemd-tmpfiles-setup-dev.service"
+    ];
+
+    systemd.additionalUpstreamUserUnits = [
+      "systemd-tmpfiles-clean.service"
+      "systemd-tmpfiles-clean.timer"
+      "systemd-tmpfiles-setup.service"
+    ];
+
+    environment.etc = {
+      "tmpfiles.d".source = (pkgs.symlinkJoin {
+        name = "tmpfiles.d";
+        paths = map (p: p + "/lib/tmpfiles.d") cfg.packages;
+        postBuild = ''
+          for i in $(cat $pathsPath); do
+            (test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
+              echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
+              exit 1
+            )
+          done
+        '' + concatMapStrings (name: optionalString (hasPrefix "tmpfiles.d/" name) ''
+          rm -f $out/${removePrefix "tmpfiles.d/" name}
+        '') config.system.build.etc.passthru.targets;
+      }) + "/*";
+    };
+
+    systemd.tmpfiles.packages = [
+      # Default tmpfiles rules provided by systemd
+      (pkgs.runCommand "systemd-default-tmpfiles" {} ''
+        mkdir -p $out/lib/tmpfiles.d
+        cd $out/lib/tmpfiles.d
+
+        ln -s "${systemd}/example/tmpfiles.d/home.conf"
+        ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
+        ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/var.conf"
+        ln -s "${systemd}/example/tmpfiles.d/x11.conf"
+      '')
+      # User-specified tmpfiles rules
+      (pkgs.writeTextFile {
+        name = "nixos-tmpfiles.d";
+        destination = "/lib/tmpfiles.d/00-nixos.conf";
+        text = ''
+          # This file is created automatically and should not be modified.
+          # Please change the option ‘systemd.tmpfiles.rules’ instead.
+
+          ${concatStringsSep "\n" cfg.rules}
+        '';
+      })
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d  /nix/var                           0755 root root - -"
+      "L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system"
+      "d  /run/lock                          0755 root root - -"
+      "d  /var/db                            0755 root root - -"
+      "L  /etc/mtab                          -    -    -    - ../proc/mounts"
+      "L  /var/lock                          -    -    -    - ../run/lock"
+      # Boot-time cleanup
+      "R! /etc/group.lock                    -    -    -    - -"
+      "R! /etc/passwd.lock                   -    -    -    - -"
+      "R! /etc/shadow.lock                   -    -    -    - -"
+      "R! /etc/mtab*                         -    -    -    - -"
+      "R! /nix/var/nix/gcroots/tmp           -    -    -    - -"
+      "R! /nix/var/nix/temproots             -    -    -    - -"
+    ];
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/user.nix b/nixos/modules/system/boot/systemd/user.nix
new file mode 100644
index 0000000000000..edfff5abaa9e3
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/user.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, utils, ... }:
+with utils;
+with systemdUtils.unitOptions;
+with lib;
+
+let
+  cfg = config.systemd.user;
+
+  systemd = config.systemd.package;
+
+  inherit
+    (systemdUtils.lib)
+    makeUnit
+    generateUnits
+    targetToUnit
+    serviceToUnit
+    sliceToUnit
+    socketToUnit
+    timerToUnit
+    pathToUnit;
+
+  upstreamUserUnits = [
+    "app.slice"
+    "background.slice"
+    "basic.target"
+    "bluetooth.target"
+    "default.target"
+    "exit.target"
+    "graphical-session-pre.target"
+    "graphical-session.target"
+    "paths.target"
+    "printer.target"
+    "session.slice"
+    "shutdown.target"
+    "smartcard.target"
+    "sockets.target"
+    "sound.target"
+    "systemd-exit.service"
+    "timers.target"
+    "xdg-desktop-autostart.target"
+  ] ++ config.systemd.additionalUpstreamUserUnits;
+in {
+  options = {
+    systemd.user.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "DefaultCPUAccounting=yes";
+      description = ''
+        Extra config options for systemd user instances. See man systemd-user.conf for
+        available options.
+      '';
+    };
+
+    systemd.user.units = mkOption {
+      description = "Definition of systemd per-user units.";
+      default = {};
+      type = systemdUtils.types.units;
+    };
+
+    systemd.user.paths = mkOption {
+      default = {};
+      type = systemdUtils.types.paths;
+      description = "Definition of systemd per-user path units.";
+    };
+
+    systemd.user.services = mkOption {
+      default = {};
+      type = systemdUtils.types.services;
+      description = "Definition of systemd per-user service units.";
+    };
+
+    systemd.user.slices = mkOption {
+      default = {};
+      type = systemdUtils.types.slices;
+      description = "Definition of systemd per-user slice units.";
+    };
+
+    systemd.user.sockets = mkOption {
+      default = {};
+      type = systemdUtils.types.sockets;
+      description = "Definition of systemd per-user socket units.";
+    };
+
+    systemd.user.targets = mkOption {
+      default = {};
+      type = systemdUtils.types.targets;
+      description = "Definition of systemd per-user target units.";
+    };
+
+    systemd.user.timers = mkOption {
+      default = {};
+      type = systemdUtils.types.timers;
+      description = "Definition of systemd per-user timer units.";
+    };
+
+    systemd.additionalUpstreamUserUnits = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      example = [];
+      description = ''
+        Additional units shipped with systemd that should be enabled for per-user systemd instances.
+      '';
+      internal = true;
+    };
+  };
+
+  config = {
+    systemd.additionalUpstreamSystemUnits = [
+      "user.slice"
+    ];
+
+    environment.etc = {
+      "systemd/user".source = generateUnits {
+        type = "user";
+        inherit (cfg) units;
+        upstreamUnits = upstreamUserUnits;
+        upstreamWants = [];
+      };
+
+      "systemd/user.conf".text = ''
+        [Manager]
+        ${cfg.extraConfig}
+      '';
+    };
+
+    systemd.user.units =
+         mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
+      // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
+      // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.slices
+      // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.sockets
+      // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.targets
+      // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.timers;
+
+    # Generate timer units for all services that have a ‘startAt’ value.
+    systemd.user.timers =
+      mapAttrs (name: service: {
+        wantedBy = ["timers.target"];
+        timerConfig.OnCalendar = service.startAt;
+      })
+      (filterAttrs (name: service: service.startAt != []) cfg.services);
+
+    # Provide the systemd-user PAM service, required to run systemd
+    # user instances.
+    security.pam.services.systemd-user =
+      { # Ensure that pam_systemd gets included. This is special-cased
+        # in systemd to provide XDG_RUNTIME_DIR.
+        startSession = true;
+        # Disable pam_mount in systemd-user to prevent it from being called
+        # multiple times during login, because it will prevent pam_mount from
+        # unmounting the previously mounted volumes.
+        pamMount = false;
+      };
+
+    # Some overrides to upstream units.
+    systemd.services."user@".restartIfChanged = false;
+    systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
+  };
+}
diff --git a/nixos/modules/system/boot/timesyncd.nix b/nixos/modules/system/boot/timesyncd.nix
index 5f35a15476965..6279957fcd63b 100644
--- a/nixos/modules/system/boot/timesyncd.nix
+++ b/nixos/modules/system/boot/timesyncd.nix
@@ -60,15 +60,27 @@ with lib;
     };
     users.groups.systemd-timesync.gid = config.ids.gids.systemd-timesync;
 
-    system.activationScripts.systemd-timesyncd-migration = mkIf (versionOlder config.system.stateVersion "19.09") ''
+    system.activationScripts.systemd-timesyncd-migration =
       # workaround an issue of systemd-timesyncd not starting due to upstream systemd reverting their dynamic users changes
       #  - https://github.com/NixOS/nixpkgs/pull/61321#issuecomment-492423742
       #  - https://github.com/systemd/systemd/issues/12131
-      if [ -L /var/lib/systemd/timesync ]; then
-        rm /var/lib/systemd/timesync
-        mv /var/lib/private/systemd/timesync /var/lib/systemd/timesync
+      mkIf (versionOlder config.system.stateVersion "19.09") ''
+        if [ -L /var/lib/systemd/timesync ]; then
+          rm /var/lib/systemd/timesync
+          mv /var/lib/private/systemd/timesync /var/lib/systemd/timesync
+        fi
+      '';
+    system.activationScripts.systemd-timesyncd-init-clock =
+      # Ensure that we have some stored time to prevent systemd-timesyncd to
+      # resort back to the fallback time.
+      # If the file doesn't exist we assume that our current system clock is
+      # good enough to provide an initial value.
+      ''
+      if ! [ -f /var/lib/systemd/timesync/clock ]; then
+        test -d /var/lib/systemd/timesync || mkdir -p /var/lib/systemd/timesync
+        touch /var/lib/systemd/timesync/clock
       fi
-    '';
+      '';
   };
 
 }
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 6edafd6695b62..cf6d19eb5f0ec 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -48,7 +48,12 @@ in
         what = "tmpfs";
         where = "/tmp";
         type = "tmpfs";
-        mountConfig.Options = [ "mode=1777" "strictatime" "rw" "nosuid" "nodev" "size=${toString cfg.tmpOnTmpfsSize}" ];
+        mountConfig.Options = concatStringsSep "," [ "mode=1777"
+                                                     "strictatime"
+                                                     "rw"
+                                                     "nosuid"
+                                                     "nodev"
+                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
       }
     ];
 
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
index b931b27ad8170..21a25cbfa9643 100644
--- a/nixos/modules/tasks/auto-upgrade.nix
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -63,13 +63,16 @@ in {
       };
 
       dates = mkOption {
-        default = "04:40";
         type = types.str;
+        default = "04:40";
+        example = "daily";
         description = ''
-          Specification (in the format described by
+          How often or when upgrade occurs. For most desktop and server systems
+          a sufficient upgrade frequency is once a day.
+
+          The format is described in
           <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>) of the time at
-          which the update will occur.
+          <manvolnum>7</manvolnum></citerefentry>.
         '';
       };
 
@@ -80,6 +83,7 @@ in {
           Reboot the system into the new generation instead of a switch
           if the new generation uses a different kernel, kernel modules
           or initrd than the booted system.
+          See <option>rebootWindow</option> for configuring the times at which a reboot is allowed.
         '';
       };
 
@@ -89,13 +93,55 @@ in {
         example = "45min";
         description = ''
           Add a randomized delay before each automatic upgrade.
-          The delay will be chozen between zero and this value.
+          The delay will be chosen between zero and this value.
           This value must be a time span in the format specified by
           <citerefentry><refentrytitle>systemd.time</refentrytitle>
           <manvolnum>7</manvolnum></citerefentry>
         '';
       };
 
+      rebootWindow = mkOption {
+        description = ''
+          Define a lower and upper time value (in HH:MM format) which
+          constitute a time window during which reboots are allowed after an upgrade.
+          This option only has an effect when <option>allowReboot</option> is enabled.
+          The default value of <literal>null</literal> means that reboots are allowed at any time.
+        '';
+        default = null;
+        example = { lower = "01:00"; upper = "05:00"; };
+        type = with types; nullOr (submodule {
+          options = {
+            lower = mkOption {
+              description = "Lower limit of the reboot window";
+              type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
+              example = "01:00";
+            };
+
+            upper = mkOption {
+              description = "Upper limit of the reboot window";
+              type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
+              example = "05:00";
+            };
+          };
+        });
+      };
+
+      persistent = mkOption {
+        default = true;
+        type = types.bool;
+        example = false;
+        description = ''
+          Takes a boolean argument. If true, the time when the service
+          unit was last triggered is stored on disk. When the timer is
+          activated, the service unit is triggered immediately if it
+          would have been triggered at least once during the time when
+          the timer was inactive. Such triggering is nonetheless
+          subject to the delay imposed by RandomizedDelaySec=. This is
+          useful to catch up on missed runs of the service when the
+          system was powered down.
+        '';
+      };
+
     };
 
   };
@@ -110,12 +156,10 @@ in {
     }];
 
     system.autoUpgrade.flags = (if cfg.flake == null then
-        [ "--no-build-output" ] ++ (if cfg.channel == null then
-          [ "--upgrade" ]
-        else [
+        [ "--no-build-output" ] ++ optionals (cfg.channel != null) [
           "-I"
           "nixpkgs=${cfg.channel}/nixexprs.tar.xz"
-        ])
+        ]
       else
         [ "--flake ${cfg.flake}" ]);
 
@@ -143,27 +187,67 @@ in {
       ];
 
       script = let
-        nixos-rebuild =
-          "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
+        nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
+        date     = "${pkgs.coreutils}/bin/date";
+        readlink = "${pkgs.coreutils}/bin/readlink";
+        shutdown = "${config.systemd.package}/bin/shutdown";
+        upgradeFlag = optional (cfg.channel == null) "--upgrade";
       in if cfg.allowReboot then ''
-        ${nixos-rebuild} boot ${toString cfg.flags}
-        booted="$(readlink /run/booted-system/{initrd,kernel,kernel-modules})"
-        built="$(readlink /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
-        if [ "$booted" = "$built" ]; then
+        ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)}
+        booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})"
+        built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
+
+        ${optionalString (cfg.rebootWindow != null) ''
+          current_time="$(${date} +%H:%M)"
+
+          lower="${cfg.rebootWindow.lower}"
+          upper="${cfg.rebootWindow.upper}"
+
+          if [[ "''${lower}" < "''${upper}" ]]; then
+            if [[ "''${current_time}" > "''${lower}" ]] && \
+               [[ "''${current_time}" < "''${upper}" ]]; then
+              do_reboot="true"
+            else
+              do_reboot="false"
+            fi
+          else
+            # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
+            # we want to reboot if cur > 23h or cur < 6h
+            if [[ "''${current_time}" < "''${upper}" ]] || \
+               [[ "''${current_time}" > "''${lower}" ]]; then
+              do_reboot="true"
+            else
+              do_reboot="false"
+            fi
+          fi
+        ''}
+
+        if [ "''${booted}" = "''${built}" ]; then
           ${nixos-rebuild} switch ${toString cfg.flags}
+        ${optionalString (cfg.rebootWindow != null) ''
+          elif [ "''${do_reboot}" != true ]; then
+            echo "Outside of configured reboot window, skipping."
+        ''}
         else
-          /run/current-system/sw/bin/shutdown -r +1
+          ${shutdown} -r +1
         fi
       '' else ''
-        ${nixos-rebuild} switch ${toString cfg.flags}
+        ${nixos-rebuild} switch ${toString (cfg.flags ++ upgradeFlag)}
       '';
 
       startAt = cfg.dates;
-    };
 
-    systemd.timers.nixos-upgrade.timerConfig.RandomizedDelaySec =
-      cfg.randomizedDelaySec;
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+    };
 
+    systemd.timers.nixos-upgrade = {
+      timerConfig = {
+        RandomizedDelaySec = cfg.randomizedDelaySec;
+        Persistent = cfg.persistent;
+      };
+    };
   };
 
 }
+
diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix
index 41fb7664f3d17..0a13522de11f0 100644
--- a/nixos/modules/tasks/bcache.nix
+++ b/nixos/modules/tasks/bcache.nix
@@ -1,13 +1,23 @@
-{ pkgs, ... }:
+{ config, lib, pkgs, ... }:
 
 {
+  options.boot.initrd.services.bcache.enable = (lib.mkEnableOption "bcache support in the initrd") // {
+    visible = false; # only works with systemd stage 1
+  };
 
-  environment.systemPackages = [ pkgs.bcache-tools ];
+  config = {
 
-  services.udev.packages = [ pkgs.bcache-tools ];
+    environment.systemPackages = [ pkgs.bcache-tools ];
 
-  boot.initrd.extraUdevRulesCommands = ''
-    cp -v ${pkgs.bcache-tools}/lib/udev/rules.d/*.rules $out/
-  '';
+    services.udev.packages = [ pkgs.bcache-tools ];
 
+    boot.initrd.extraUdevRulesCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
+      cp -v ${pkgs.bcache-tools}/lib/udev/rules.d/*.rules $out/
+    '';
+
+    boot.initrd.services.udev = lib.mkIf config.boot.initrd.services.bcache.enable {
+      packages = [ pkgs.bcache-tools ];
+      binPackages = [ pkgs.bcache-tools ];
+    };
+  };
 }
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index f3da6771197e6..82adc499ad0e7 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -215,6 +215,35 @@ in
       '';
     };
 
+    boot.devSize = mkOption {
+      default = "5%";
+      example = "32m";
+      type = types.str;
+      description = ''
+        Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option,
+        for the accepted syntax.
+      '';
+    };
+
+    boot.devShmSize = mkOption {
+      default = "50%";
+      example = "256m";
+      type = types.str;
+      description = ''
+        Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option,
+        for the accepted syntax.
+      '';
+    };
+
+    boot.runSize = mkOption {
+      default = "25%";
+      example = "256m";
+      type = types.str;
+      description = ''
+        Size limit for the /run tmpfs. Look at mount(8), tmpfs size option,
+        for the accepted syntax.
+      '';
+    };
   };
 
 
@@ -251,7 +280,8 @@ in
     environment.etc.fstab.text =
       let
         fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" "apfs" ];
-        skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
+        isBindMount = fs: builtins.elem "bind" fs.options;
+        skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
         # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
         escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
         swapOptions = sw: concatStringsSep "," (
@@ -323,7 +353,7 @@ in
             unitConfig.DefaultDependencies = false; # needed to prevent a cycle
             serviceConfig.Type = "oneshot";
           };
-      in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // {
+      in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // {
     # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
     # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
         "mount-pstore" = {
diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix
index ae1dab5b8d8d4..b7ebc37dd5cf9 100644
--- a/nixos/modules/tasks/filesystems/btrfs.nix
+++ b/nixos/modules/tasks/filesystems/btrfs.nix
@@ -66,19 +66,19 @@ in
         ]
       );
 
-      boot.initrd.extraUtilsCommands = mkIf inInitrd
+      boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
         ln -sv btrfs $out/bin/btrfsck
         ln -sv btrfsck $out/bin/fsck.btrfs
       '';
 
-      boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
+      boot.initrd.extraUtilsCommandsTest = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         $out/bin/btrfs --version
       '';
 
-      boot.initrd.postDeviceCommands = mkIf inInitrd
+      boot.initrd.postDeviceCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         btrfs device scan
       '';
diff --git a/nixos/modules/tasks/filesystems/cifs.nix b/nixos/modules/tasks/filesystems/cifs.nix
index 47ba0c03c5630..0de292a692082 100644
--- a/nixos/modules/tasks/filesystems/cifs.nix
+++ b/nixos/modules/tasks/filesystems/cifs.nix
@@ -16,7 +16,7 @@ in
     boot.initrd.availableKernelModules = mkIf inInitrd
       [ "cifs" "nls_utf8" "hmac" "md4" "ecb" "des_generic" "sha256" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         copy_bin_and_libs ${pkgs.cifs-utils}/sbin/mount.cifs
       '';
diff --git a/nixos/modules/tasks/filesystems/ext.nix b/nixos/modules/tasks/filesystems/ext.nix
index a14a3ac38549c..9b61f21643aba 100644
--- a/nixos/modules/tasks/filesystems/ext.nix
+++ b/nixos/modules/tasks/filesystems/ext.nix
@@ -1,14 +1,20 @@
-{ pkgs, ... }:
+{ config, lib, pkgs, ... }:
+
+let
+
+  inInitrd = lib.any (fs: fs == "ext2" || fs == "ext3" || fs == "ext4") config.boot.initrd.supportedFilesystems;
+
+in
 
 {
   config = {
 
-    system.fsPackages = [ pkgs.e2fsprogs ];
+    system.fsPackages = lib.mkIf (config.boot.initrd.systemd.enable -> inInitrd) [ pkgs.e2fsprogs ];
 
     # As of kernel 4.3, there is no separate ext3 driver (they're also handled by ext4.ko)
-    boot.initrd.availableKernelModules = [ "ext2" "ext4" ];
+    boot.initrd.availableKernelModules = lib.mkIf (config.boot.initrd.systemd.enable -> inInitrd) [ "ext2" "ext4" ];
 
-    boot.initrd.extraUtilsCommands =
+    boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable)
       ''
         # Copy e2fsck and friends.
         copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/e2fsck
diff --git a/nixos/modules/tasks/filesystems/f2fs.nix b/nixos/modules/tasks/filesystems/f2fs.nix
index a305235979a2f..1d52861aa39d1 100644
--- a/nixos/modules/tasks/filesystems/f2fs.nix
+++ b/nixos/modules/tasks/filesystems/f2fs.nix
@@ -13,7 +13,7 @@ in
 
     boot.initrd.availableKernelModules = mkIf inInitrd [ "f2fs" "crc32" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd ''
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/fsck.f2fs
       ${optionalString (any (fs: fs.autoResize) fileSystems) ''
         # We need f2fs-tools' tools to resize filesystems
diff --git a/nixos/modules/tasks/filesystems/jfs.nix b/nixos/modules/tasks/filesystems/jfs.nix
index fc3905c7dc201..700f05af2bec4 100644
--- a/nixos/modules/tasks/filesystems/jfs.nix
+++ b/nixos/modules/tasks/filesystems/jfs.nix
@@ -12,7 +12,7 @@ in
 
     boot.initrd.kernelModules = mkIf inInitrd [ "jfs" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd ''
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.jfsutils}/sbin/fsck.jfs
     '';
   };
diff --git a/nixos/modules/tasks/filesystems/reiserfs.nix b/nixos/modules/tasks/filesystems/reiserfs.nix
index ab4c43e2ab826..7b017a83db848 100644
--- a/nixos/modules/tasks/filesystems/reiserfs.nix
+++ b/nixos/modules/tasks/filesystems/reiserfs.nix
@@ -15,7 +15,7 @@ in
 
     boot.initrd.kernelModules = mkIf inInitrd [ "reiserfs" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         copy_bin_and_libs ${pkgs.reiserfsprogs}/sbin/reiserfsck
         ln -s reiserfsck $out/bin/fsck.reiserfs
diff --git a/nixos/modules/tasks/filesystems/unionfs-fuse.nix b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
index f54f3559c3411..f9954b5182f91 100644
--- a/nixos/modules/tasks/filesystems/unionfs-fuse.nix
+++ b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
@@ -6,7 +6,7 @@
     (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.initrd.supportedFilesystems) {
       boot.initrd.kernelModules = [ "fuse" ];
 
-      boot.initrd.extraUtilsCommands = ''
+      boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
         copy_bin_and_libs ${pkgs.fuse}/sbin/mount.fuse
         copy_bin_and_libs ${pkgs.unionfs-fuse}/bin/unionfs
         substitute ${pkgs.unionfs-fuse}/sbin/mount.unionfs-fuse $out/bin/mount.unionfs-fuse \
@@ -16,12 +16,23 @@
         chmod +x $out/bin/mount.unionfs-fuse
       '';
 
-      boot.initrd.postDeviceCommands = ''
+      boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
           # Hacky!!! fuse hard-codes the path to mount
           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
         '';
+
+      boot.initrd.systemd.extraBin = {
+        "mount.fuse" = "${pkgs.fuse}/bin/mount.fuse";
+        "unionfs" = "${pkgs.unionfs-fuse}/bin/unionfs";
+        "mount.unionfs-fuse" = pkgs.runCommand "mount.unionfs-fuse" {} ''
+          substitute ${pkgs.unionfs-fuse}/sbin/mount.unionfs-fuse $out \
+            --replace '${pkgs.bash}/bin/bash' /bin/sh \
+            --replace '${pkgs.fuse}/sbin' /bin \
+            --replace '${pkgs.unionfs-fuse}/bin' /bin
+        '';
+      };
     })
 
     (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.supportedFilesystems) {
diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix
index 958e27ae8a32a..5baab1c802cf9 100644
--- a/nixos/modules/tasks/filesystems/vfat.nix
+++ b/nixos/modules/tasks/filesystems/vfat.nix
@@ -15,7 +15,7 @@ in
 
     boot.initrd.kernelModules = mkIf inInitrd [ "vfat" "nls_cp437" "nls_iso8859-1" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         copy_bin_and_libs ${pkgs.dosfstools}/sbin/dosfsck
         ln -sv dosfsck $out/bin/fsck.vfat
diff --git a/nixos/modules/tasks/filesystems/xfs.nix b/nixos/modules/tasks/filesystems/xfs.nix
index 98038701ca580..f81f586465519 100644
--- a/nixos/modules/tasks/filesystems/xfs.nix
+++ b/nixos/modules/tasks/filesystems/xfs.nix
@@ -15,14 +15,14 @@ in
 
     boot.initrd.availableKernelModules = mkIf inInitrd [ "xfs" "crc32c" ];
 
-    boot.initrd.extraUtilsCommands = mkIf inInitrd
+    boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/fsck.xfs
         copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/xfs_repair
       '';
 
     # Trick just to set 'sh' after the extraUtils nuke-refs.
-    boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
+    boot.initrd.extraUtilsCommandsTest = mkIf (inInitrd && !config.boot.initrd.systemd.enable)
       ''
         sed -i -e 's,^#!.*,#!'$out/bin/sh, $out/bin/fsck.xfs
       '';
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 3bc0dedec00e2..05174e03754b6 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -58,6 +58,13 @@ let
   # latter case it makes one last attempt at importing, allowing the system to
   # (eventually) boot even with a degraded pool.
   importLib = {zpoolCmd, awkCmd, cfgZfs}: ''
+    for o in $(cat /proc/cmdline); do
+      case $o in
+        zfs_force|zfs_force=1|zfs_force=y)
+          ZFS_FORCE="-f"
+          ;;
+      esac
+    done
     poolReady() {
       pool="$1"
       state="$("${zpoolCmd}" import 2>/dev/null | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")"
@@ -78,6 +85,95 @@ let
     }
   '';
 
+  getPoolFilesystems = pool:
+    filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems;
+
+  getPoolMounts = prefix: pool:
+    let
+      # Remove the "/" suffix because even though most mountpoints
+      # won't have it, the "/" mountpoint will, and we can't have the
+      # trailing slash in "/sysroot/" in stage 1.
+      mountPoint = fs: escapeSystemdPath (prefix + (lib.removeSuffix "/" fs.mountPoint));
+    in
+      map (x: "${mountPoint x}.mount") (getPoolFilesystems pool);
+
+  getKeyLocations = pool:
+    if isBool cfgZfs.requestEncryptionCredentials
+    then "${cfgZfs.package}/sbin/zfs list -rHo name,keylocation,keystatus ${pool}"
+    else "${cfgZfs.package}/sbin/zfs list -Ho name,keylocation,keystatus ${toString (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials)}";
+
+  createImportService = { pool, systemd, force, prefix ? "" }:
+    nameValuePair "zfs-import-${pool}" {
+      description = "Import ZFS pool \"${pool}\"";
+      # we need systemd-udev-settle to ensure devices are available
+      # In the future, hopefully someone will complete this:
+      # https://github.com/zfsonlinux/zfs/pull/4943
+      requires = [ "systemd-udev-settle.service" ];
+      after = [
+        "systemd-udev-settle.service"
+        "systemd-modules-load.service"
+        "systemd-ask-password-console.service"
+      ];
+      wantedBy = (getPoolMounts prefix pool) ++ [ "local-fs.target" ];
+      before = (getPoolMounts prefix pool) ++ [ "local-fs.target" ];
+      unitConfig = {
+        DefaultDependencies = "no";
+      };
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+      environment.ZFS_FORCE = optionalString force "-f";
+      script = (importLib {
+        # See comments at importLib definition.
+        zpoolCmd = "${cfgZfs.package}/sbin/zpool";
+        awkCmd = "${pkgs.gawk}/bin/awk";
+        inherit cfgZfs;
+      }) + ''
+        poolImported "${pool}" && exit
+        echo -n "importing ZFS pool \"${pool}\"..."
+        # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
+        for trial in `seq 1 60`; do
+          poolReady "${pool}" && poolImport "${pool}" && break
+          sleep 1
+        done
+        poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
+        if poolImported "${pool}"; then
+          ${optionalString (if isBool cfgZfs.requestEncryptionCredentials
+                            then cfgZfs.requestEncryptionCredentials
+                            else cfgZfs.requestEncryptionCredentials != []) ''
+            ${getKeyLocations pool} | while IFS=$'\t' read ds kl ks; do
+              {
+              if [[ "$ks" != unavailable ]]; then
+                continue
+              fi
+              case "$kl" in
+                none )
+                  ;;
+                prompt )
+                  tries=3
+                  success=false
+                  while [[ $success != true ]] && [[ $tries -gt 0 ]]; do
+                    ${systemd}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
+                      && success=true \
+                      || tries=$((tries - 1))
+                  done
+                  [[ $success = true ]]
+                  ;;
+                * )
+                  ${cfgZfs.package}/sbin/zfs load-key "$ds"
+                  ;;
+              esac
+              } < /dev/null # To protect while read ds kl in case anything reads stdin
+            done
+          ''}
+          echo "Successfully imported ${pool}"
+        else
+          exit 1
+        fi
+      '';
+    };
+
   zedConf = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {
       mkValueString = v:
@@ -428,14 +524,6 @@ in
           '';
         postDeviceCommands = concatStringsSep "\n" ([''
             ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}"
-
-            for o in $(cat /proc/cmdline); do
-              case $o in
-                zfs_force|zfs_force=1)
-                  ZFS_FORCE="-f"
-                  ;;
-              esac
-            done
           ''] ++ [(importLib {
             # See comments at importLib definition.
             zpoolCmd = "zpool";
@@ -461,11 +549,31 @@ in
                 zfs load-key -a
               ''
               else concatMapStrings (fs: ''
-                zfs load-key ${fs}
+                zfs load-key -- ${escapeShellArg fs}
               '') cfgZfs.requestEncryptionCredentials}
         '') rootPools));
+
+        # Systemd in stage 1
+        systemd = {
+          packages = [cfgZfs.package];
+          services = listToAttrs (map (pool: createImportService {
+            inherit pool;
+            systemd = config.boot.initrd.systemd.package;
+            force = cfgZfs.forceImportRoot;
+            prefix = "/sysroot";
+          }) rootPools);
+          extraBin = {
+            # zpool and zfs are already in thanks to fsPackages
+            awk = "${pkgs.gawk}/bin/awk";
+          };
+        };
       };
 
+      systemd.shutdownRamfs.contents."/etc/systemd/system-shutdown/zpool".source = pkgs.writeShellScript "zpool-sync-shutdown" ''
+        exec ${cfgZfs.package}/bin/zpool sync
+      '';
+      systemd.shutdownRamfs.storePaths = ["${cfgZfs.package}/bin/zpool"];
+
       # TODO FIXME See https://github.com/NixOS/nixpkgs/pull/99386#issuecomment-798813567. To not break people's bootloader and as probably not everybody would read release notes that thoroughly add inSystem.
       boot.loader.grub = mkIf (inInitrd || inSystem) {
         zfsSupport = true;
@@ -516,79 +624,11 @@ in
       systemd.packages = [ cfgZfs.package ];
 
       systemd.services = let
-        getPoolFilesystems = pool:
-          filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems;
-
-        getPoolMounts = pool:
-          let
-            mountPoint = fs: escapeSystemdPath fs.mountPoint;
-          in
-            map (x: "${mountPoint x}.mount") (getPoolFilesystems pool);
-
-        createImportService = pool:
-          nameValuePair "zfs-import-${pool}" {
-            description = "Import ZFS pool \"${pool}\"";
-            # we need systemd-udev-settle until https://github.com/zfsonlinux/zfs/pull/4943 is merged
-            requires = [ "systemd-udev-settle.service" ];
-            after = [
-              "systemd-udev-settle.service"
-              "systemd-modules-load.service"
-              "systemd-ask-password-console.service"
-            ];
-            wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ];
-            before = (getPoolMounts pool) ++ [ "local-fs.target" ];
-            unitConfig = {
-              DefaultDependencies = "no";
-            };
-            serviceConfig = {
-              Type = "oneshot";
-              RemainAfterExit = true;
-            };
-            environment.ZFS_FORCE = optionalString cfgZfs.forceImportAll "-f";
-            script = (importLib {
-              # See comments at importLib definition.
-              zpoolCmd = "${cfgZfs.package}/sbin/zpool";
-              awkCmd = "${pkgs.gawk}/bin/awk";
-              inherit cfgZfs;
-            }) + ''
-              poolImported "${pool}" && exit
-              echo -n "importing ZFS pool \"${pool}\"..."
-              # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
-              for trial in `seq 1 60`; do
-                poolReady "${pool}" && poolImport "${pool}" && break
-                sleep 1
-              done
-              poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
-              if poolImported "${pool}"; then
-                ${optionalString (if isBool cfgZfs.requestEncryptionCredentials
-                                  then cfgZfs.requestEncryptionCredentials
-                                  else cfgZfs.requestEncryptionCredentials != []) ''
-                  ${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
-                         fi
-                       ''}
-                    case "$kl" in
-                      none )
-                        ;;
-                      prompt )
-                        ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/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
-                ''}
-                echo "Successfully imported ${pool}"
-              else
-                exit 1
-              fi
-            '';
-          };
+        createImportService' = pool: createImportService {
+          inherit pool;
+          systemd = config.systemd.package;
+          force = cfgZfs.forceImportAll;
+        };
 
         # This forces a sync of any ZFS pools prior to poweroff, even if they're set
         # to sync=disabled.
@@ -614,7 +654,7 @@ in
             wantedBy = [ "zfs.target" ];
           };
 
-      in listToAttrs (map createImportService dataPools ++
+      in listToAttrs (map createImportService' dataPools ++
                       map createSyncService allPools ++
                       map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]);
 
@@ -642,41 +682,14 @@ in
         };
 
         scriptArgs = "%i";
-        path = [ pkgs.gawk cfgZfs.package ];
-
-        # ZFS has no way of enumerating just devices in a pool in a way
-        # that 'zpool online -e' supports. Thus, we've implemented a
-        # bit of a strange approach of highlighting just devices.
-        # See: https://github.com/openzfs/zfs/issues/12505
-        script = let
-          # This UUID has been chosen at random and is to provide a
-          # collision-proof, predictable token to search for
-          magicIdentifier = "NIXOS-ZFS-ZPOOL-DEVICE-IDENTIFIER-37108bec-aff6-4b58-9e5e-53c7c9766f05";
-          zpoolScripts = pkgs.writeShellScriptBin "device-highlighter" ''
-            echo "${magicIdentifier}"
-          '';
-        in ''
+        path = [ cfgZfs.package ];
+
+        script =  ''
           pool=$1
 
           echo "Expanding all devices for $pool."
 
-          # Put our device-highlighter script it to the PATH
-          export ZPOOL_SCRIPTS_PATH=${zpoolScripts}/bin
-
-          # Enable running our precisely specified zpool script as root
-          export ZPOOL_SCRIPTS_AS_ROOT=1
-
-          devices() (
-            zpool status -c device-highlighter "$pool" \
-             | awk '($2 == "ONLINE" && $6 == "${magicIdentifier}") { print $1; }'
-          )
-
-          for device in $(devices); do
-            echo "Attempting to expand $device of $pool..."
-            if ! zpool online -e "$pool" "$device"; then
-              echo "Failed to expand '$device' of '$pool'."
-            fi
-          done
+          ${pkgs.zpool-auto-expand-partitions}/bin/zpool_part_disks --automatically-grow "$pool"
         '';
       };
 
@@ -688,7 +701,7 @@ in
           # expand every pool. Otherwise we want to enumerate
           # just the specifically provided list of pools.
           poolListProvider = if cfgExpandOnBoot == "all"
-            then "$(zpool list -H | awk '{print $1}')"
+            then "$(zpool list -H -o name)"
             else lib.escapeShellArgs cfgExpandOnBoot;
         in
         {
@@ -701,8 +714,6 @@ in
             RemainAfterExit = true;
           };
 
-          path = [ pkgs.gawk cfgZfs.package ];
-
           script = ''
             for pool in ${poolListProvider}; do
               systemctl start --no-block "zpool-expand@$pool"
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
index 35316603c38f2..2f98e3e7da5b2 100644
--- a/nixos/modules/tasks/lvm.nix
+++ b/nixos/modules/tasks/lvm.nix
@@ -7,17 +7,22 @@ in {
   options.services.lvm = {
     package = mkOption {
       type = types.package;
-      default = if cfg.dmeventd.enable then pkgs.lvm2_dmeventd else pkgs.lvm2;
+      default = pkgs.lvm2;
       internal = true;
       defaultText = literalExpression "pkgs.lvm2";
       description = ''
         This option allows you to override the LVM package that's used on the system
         (udev rules, tmpfiles, systemd services).
-        Defaults to pkgs.lvm2, or pkgs.lvm2_dmeventd if dmeventd is enabled.
+        Defaults to pkgs.lvm2, pkgs.lvm2_dmeventd if dmeventd or pkgs.lvm2_vdo if vdo is enabled.
       '';
     };
     dmeventd.enable = mkEnableOption "the LVM dmevent daemon";
     boot.thin.enable = mkEnableOption "support for booting from ThinLVs";
+    boot.vdo.enable = mkEnableOption "support for booting from VDOLVs";
+  };
+
+  options.boot.initrd.services.lvm.enable = (mkEnableOption "enable booting from LVM2 in the initrd") // {
+    visible = false;
   };
 
   config = mkMerge [
@@ -30,8 +35,13 @@ in {
       environment.systemPackages = [ cfg.package ];
       systemd.packages = [ cfg.package ];
 
-      # TODO: update once https://github.com/NixOS/nixpkgs/pull/93006 was merged
       services.udev.packages = [ cfg.package.out ];
+
+      # We need lvm2 for the device-mapper rules
+      boot.initrd.services.udev.packages = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
+      # The device-mapper rules want to call tools from lvm2
+      boot.initrd.systemd.initrdBin = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
+      boot.initrd.services.udev.binPackages = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
     })
     (mkIf cfg.dmeventd.enable {
       systemd.sockets."dm-event".wantedBy = [ "sockets.target" ];
@@ -40,18 +50,21 @@ in {
       environment.etc."lvm/lvm.conf".text = ''
         dmeventd/executable = "${cfg.package}/bin/dmeventd"
       '';
+      services.lvm.package = mkDefault pkgs.lvm2_dmeventd;
     })
     (mkIf cfg.boot.thin.enable {
       boot.initrd = {
         kernelModules = [ "dm-snapshot" "dm-thin-pool" ];
 
-        extraUtilsCommands = ''
+        systemd.initrdBin = lib.mkIf config.boot.initrd.services.lvm.enable [ pkgs.thin-provisioning-tools ];
+
+        extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
           for BIN in ${pkgs.thin-provisioning-tools}/bin/*; do
             copy_bin_and_libs $BIN
           done
         '';
 
-        extraUtilsCommandsTest = ''
+        extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
           ls ${pkgs.thin-provisioning-tools}/bin/ | grep -v pdata_tools | while read BIN; do
             $out/bin/$(basename $BIN) --help > /dev/null
           done
@@ -61,9 +74,47 @@ in {
       environment.etc."lvm/lvm.conf".text = concatMapStringsSep "\n"
         (bin: "global/${bin}_executable = ${pkgs.thin-provisioning-tools}/bin/${bin}")
         [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ];
+
+      environment.systemPackages = [ pkgs.thin-provisioning-tools ];
+    })
+    (mkIf cfg.boot.vdo.enable {
+      boot = {
+        initrd = {
+          kernelModules = [ "kvdo" ];
+
+          systemd.initrdBin = lib.mkIf config.boot.initrd.services.lvm.enable [ pkgs.vdo ];
+
+          extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable)''
+            ls ${pkgs.vdo}/bin/ | while read BIN; do
+              copy_bin_and_libs ${pkgs.vdo}/bin/$BIN
+            done
+            substituteInPlace $out/bin/vdorecover --replace "${pkgs.bash}/bin/bash" "/bin/sh"
+            substituteInPlace $out/bin/adaptLVMVDO.sh --replace "${pkgs.bash}/bin/bash" "/bin/sh"
+          '';
+
+          extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable)''
+            ls ${pkgs.vdo}/bin/ | grep -vE '(adaptLVMVDO|vdorecover)' | while read BIN; do
+              $out/bin/$(basename $BIN) --help > /dev/null
+            done
+          '';
+        };
+        extraModulePackages = [ config.boot.kernelPackages.kvdo ];
+      };
+
+      services.lvm.package = mkOverride 999 pkgs.lvm2_vdo;  # this overrides mkDefault
+
+      environment.systemPackages = [ pkgs.vdo ];
     })
     (mkIf (cfg.dmeventd.enable || cfg.boot.thin.enable) {
-      boot.initrd.preLVMCommands = ''
+      boot.initrd.systemd.contents."/etc/lvm/lvm.conf".text = optionalString (config.boot.initrd.services.lvm.enable && cfg.boot.thin.enable) (concatMapStringsSep "\n"
+          (bin: "global/${bin}_executable = /bin/${bin}")
+          [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ]
+        ) + "\n" + optionalString cfg.dmeventd.enable ''
+          dmeventd/executable = /bin/false
+          activation/monitoring = 0
+        '';
+
+      boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) ''
           mkdir -p /etc/lvm
           cat << EOF >> /etc/lvm/lvm.conf
           ${optionalString cfg.boot.thin.enable (
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
index 19f2be2c4a251..f44dafc9706a0 100644
--- a/nixos/modules/tasks/network-interfaces-scripted.nix
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -85,12 +85,14 @@ let
         hasDefaultGatewaySet = (cfg.defaultGateway != null && cfg.defaultGateway.address != "")
                             || (cfg.enableIPv6 && cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "");
 
-        networkLocalCommands = {
+        needNetworkSetup = cfg.resolvconf.enable || cfg.defaultGateway != null || cfg.defaultGateway6 != null;
+
+        networkLocalCommands = lib.mkIf needNetworkSetup {
           after = [ "network-setup.service" ];
           bindsTo = [ "network-setup.service" ];
         };
 
-        networkSetup =
+        networkSetup = lib.mkIf needNetworkSetup
           { description = "Networking Setup";
 
             after = [ "network-pre.target" "systemd-udevd.service" "systemd-sysctl.service" ];
@@ -219,14 +221,15 @@ let
                     cidr = "${route.address}/${toString route.prefixLength}";
                     via = optionalString (route.via != null) ''via "${route.via}"'';
                     options = concatStrings (mapAttrsToList (name: val: "${name} ${val} ") route.options);
+                    type = toString route.type;
                   in
                   ''
                      echo "${cidr}" >> $state
                      echo -n "adding route ${cidr}... "
-                     if out=$(ip route add "${cidr}" ${options} ${via} dev "${i.name}" proto static 2>&1); then
+                     if out=$(ip route add ${type} "${cidr}" ${options} ${via} dev "${i.name}" proto static 2>&1); then
                        echo "done"
                      elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then
-                       echo "'ip route add "${cidr}" ${options} ${via} dev "${i.name}"' failed: $out"
+                       echo "'ip route add ${type} "${cidr}" ${options} ${via} dev "${i.name}"' failed: $out"
                        exit 1
                      fi
                   ''
@@ -535,6 +538,7 @@ let
         createGreDevice = n: v: nameValuePair "${n}-netdev"
           (let
             deps = deviceDependency v.dev;
+            ttlarg = if lib.hasPrefix "ip6" v.type then "hoplimit" else "ttl";
           in
           { description = "GRE Tunnel Interface ${n}";
             wantedBy = [ "network-setup.service" (subsystemDevice n) ];
@@ -551,6 +555,7 @@ let
               ip link add name "${n}" type ${v.type} \
                 ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
                 ${optionalString (v.local != null) "local \"${v.local}\""} \
+                ${optionalString (v.ttl != null) "${ttlarg} ${toString v.ttl}"} \
                 ${optionalString (v.dev != null) "dev \"${v.dev}\""}
               ip link set "${n}" up
             '';
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index 8a5e1b5af114c..1657fabcd9b1c 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -43,12 +43,6 @@ in
     } {
       assertion = cfg.defaultGateway6 == null || cfg.defaultGateway6.interface == null;
       message = "networking.defaultGateway6.interface is not supported by networkd.";
-    } {
-      assertion = cfg.useDHCP == false;
-      message = ''
-        networking.useDHCP is not supported by networkd.
-        Please use per interface configuration and set the global option to false.
-      '';
     } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: {
       assertion = !rstp;
       message = "networking.bridges.${n}.rstp is not supported by networkd.";
@@ -65,21 +59,58 @@ in
         genericNetwork = override:
           let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
             ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
-          in optionalAttrs (gateway != [ ]) {
-            routes = override [
-              {
+              makeGateway = gateway: {
                 routeConfig = {
                   Gateway = gateway;
                   GatewayOnLink = false;
                 };
-              }
-            ];
+              };
+          in optionalAttrs (gateway != [ ]) {
+            routes = override (map makeGateway gateway);
           } // optionalAttrs (domains != [ ]) {
             domains = override domains;
           };
       in mkMerge [ {
         enable = true;
       }
+      (mkIf cfg.useDHCP {
+        networks."99-ethernet-default-dhcp" = lib.mkIf cfg.useDHCP {
+          # We want to match physical ethernet interfaces as commonly
+          # found on laptops, desktops and servers, to provide an
+          # "out-of-the-box" setup that works for common cases.  This
+          # heuristic isn't perfect (it could match interfaces with
+          # custom names that _happen_ to start with en or eth), but
+          # should be good enough to make the common case easy and can
+          # be overridden on a case-by-case basis using
+          # higher-priority networks or by disabling useDHCP.
+
+          # Type=ether matches veth interfaces as well, and this is
+          # more likely to result in interfaces being configured to
+          # use DHCP when they shouldn't.
+
+          # When wait-online.anyInterface is enabled, RequiredForOnline really
+          # means "sufficient for online", so we can enable it.
+          # Otherwise, don't block the network coming online because of default networks.
+          matchConfig.Name = ["en*" "eth*"];
+          DHCP = "yes";
+          linkConfig.RequiredForOnline =
+            lib.mkDefault config.systemd.network.wait-online.anyInterface;
+          networkConfig.IPv6PrivacyExtensions = "kernel";
+        };
+        networks."99-wireless-client-dhcp" = lib.mkIf cfg.useDHCP {
+          # Like above, but this is much more likely to be correct.
+          matchConfig.WLANInterfaceType = "station";
+          DHCP = "yes";
+          linkConfig.RequiredForOnline =
+            lib.mkDefault config.systemd.network.wait-online.anyInterface;
+          networkConfig.IPv6PrivacyExtensions = "kernel";
+          # We also set the route metric to one more than the default
+          # of 1024, so that Ethernet is preferred if both are
+          # available.
+          dhcpV4Config.RouteMetric = 1025;
+          ipv6AcceptRAConfig.RouteMetric = 1025;
+        };
+      })
       (mkMerge (forEach interfaces (i: {
         netdevs = mkIf i.virtual ({
           "40-${i.name}" = {
@@ -112,6 +143,9 @@ in
                 optionalAttrs (route.via != null) {
                   Gateway = route.via;
                 } //
+                optionalAttrs (route.type != null) {
+                  Type = route.type;
+                } //
                 optionalAttrs (route.options ? onlink) {
                   GatewayOnLink = true;
                 } //
@@ -318,6 +352,8 @@ in
               Remote = gre.remote;
             }) // (optionalAttrs (gre.local != null) {
               Local = gre.local;
+            }) // (optionalAttrs (gre.ttl != null) {
+              TTL = gre.ttl;
             });
         };
         networks = mkIf (gre.dev != null) {
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 5c91993771e4a..07bccf98f407f 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -90,6 +90,22 @@ let
         '';
       };
 
+      type = mkOption {
+        type = types.nullOr (types.enum [
+          "unicast" "local" "broadcast" "multicast"
+        ]);
+        default = null;
+        description = ''
+          Type of the route.  See the <literal>Route types</literal> section
+          in the <literal>ip-route(8)</literal> manual page for the details.
+
+          Note that <literal>prohibit</literal>, <literal>blackhole</literal>,
+          <literal>unreachable</literal>, and <literal>throw</literal> cannot
+          be configured per device, so they are not available here. Similarly,
+          <literal>nat</literal> hasn't been supported since kernel 2.6.
+        '';
+      };
+
       via = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -403,6 +419,15 @@ let
     </itemizedlist>
   '';
 
+  hostidFile = pkgs.runCommand "gen-hostid" { preferLocalBuild = true; } ''
+      hi="${cfg.hostId}"
+      ${if pkgs.stdenv.isBigEndian then ''
+        echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > $out
+      '' else ''
+        echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > $out
+      ''}
+    '';
+
 in
 
 {
@@ -1020,6 +1045,14 @@ in
             local = "10.0.0.22";
             dev = "enp4s0f0";
             type = "tap";
+            ttl = 255;
+          };
+          gre6Tunnel = {
+            remote = "fd7a:5634::1";
+            local = "fd7a:5634::2";
+            dev = "enp4s0f0";
+            type = "tun6";
+            ttl = 255;
           };
         }
       '';
@@ -1057,11 +1090,25 @@ in
             '';
           };
 
+          ttl = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            example = 255;
+            description = ''
+              The time-to-live/hoplimit of the connection to the remote tunnel endpoint.
+            '';
+          };
+
           type = mkOption {
-            type = with types; enum [ "tun" "tap" ];
+            type = with types; enum [ "tun" "tap" "tun6" "tap6" ];
             default = "tap";
             example = "tap";
-            apply = v: if v == "tun" then "gre" else "gretap";
+            apply = v: {
+              tun = "gre";
+              tap = "gretap";
+              tun6 = "ip6gre";
+              tap6 = "ip6gretap";
+            }.${v};
             description = ''
               Whether the tunnel routes layer 2 (tap) or layer 3 (tun) traffic.
             '';
@@ -1223,11 +1270,6 @@ in
         Whether to use DHCP to obtain an IP address and other
         configuration for all network interfaces that are not manually
         configured.
-
-        Using this option is highly discouraged and also incompatible with
-        <option>networking.useNetworkd</option>. Please use
-        <option>networking.interfaces.&lt;name&gt;.useDHCP</option> instead
-        and set this to false.
       '';
     };
 
@@ -1325,22 +1367,13 @@ in
           val = tempaddrValues.${opt}.sysctl;
          in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val));
 
-    # Capabilities won't work unless we have at-least a 4.3 Linux
-    # kernel because we need the ambient capability
-    security.wrappers = if (versionAtLeast (getVersion config.boot.kernelPackages.kernel) "4.3") then {
+    security.wrappers = {
       ping = {
         owner = "root";
         group = "root";
         capabilities = "cap_net_raw+p";
         source = "${pkgs.iputils.out}/bin/ping";
       };
-    } else {
-      ping = {
-        setuid = true;
-        owner = "root";
-        group = "root";
-        source = "${pkgs.iputils.out}/bin/ping";
-      };
     };
     security.apparmor.policies."bin.ping".profile = lib.mkIf config.security.apparmor.policies."bin.ping".enable (lib.mkAfter ''
       /run/wrappers/bin/ping {
@@ -1370,16 +1403,8 @@ in
         domainname "${cfg.domain}"
       '';
 
-    environment.etc.hostid = mkIf (cfg.hostId != null)
-      { source = pkgs.runCommand "gen-hostid" { preferLocalBuild = true; } ''
-          hi="${cfg.hostId}"
-          ${if pkgs.stdenv.isBigEndian then ''
-            echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > $out
-          '' else ''
-            echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > $out
-          ''}
-        '';
-      };
+    environment.etc.hostid = mkIf (cfg.hostId != null) { source = hostidFile; };
+    boot.initrd.systemd.contents."/etc/hostid" = mkIf (cfg.hostId != null) { source = hostidFile; };
 
     # static hostname configuration needed for hostnamectl and the
     # org.freedesktop.hostname1 dbus service (both provided by systemd)
@@ -1438,7 +1463,7 @@ in
           sysctl-value = tempaddrValues.${cfg.tempAddresses}.sysctl;
         in ''
           # enable and prefer IPv6 privacy addresses by default
-          ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'"
+          ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/$name/use_tempaddr'"
         '';
       })
       (pkgs.writeTextFile rec {
diff --git a/nixos/modules/tasks/swraid.nix b/nixos/modules/tasks/swraid.nix
index 8fa19194bed42..0b53a6d152d09 100644
--- a/nixos/modules/tasks/swraid.nix
+++ b/nixos/modules/tasks/swraid.nix
@@ -1,17 +1,43 @@
-{ pkgs, ... }:
+{ config, pkgs, lib, ... }: let
 
-{
+  cfg = config.boot.initrd.services.swraid;
 
-  environment.systemPackages = [ pkgs.mdadm ];
+in {
 
-  services.udev.packages = [ pkgs.mdadm ];
+  options.boot.initrd.services.swraid = {
+    enable = (lib.mkEnableOption "swraid support using mdadm") // {
+      visible = false; # only has effect when the new stage 1 is in place
+    };
 
-  systemd.packages = [ pkgs.mdadm ];
+    mdadmConf = lib.mkOption {
+      description = "Contents of <filename>/etc/mdadm.conf</filename> in initrd.";
+      type = lib.types.lines;
+      default = "";
+    };
+  };
 
-  boot.initrd.availableKernelModules = [ "md_mod" "raid0" "raid1" "raid10" "raid456" ];
+  config = {
+    environment.systemPackages = [ pkgs.mdadm ];
 
-  boot.initrd.extraUdevRulesCommands = ''
-    cp -v ${pkgs.mdadm}/lib/udev/rules.d/*.rules $out/
-  '';
+    services.udev.packages = [ pkgs.mdadm ];
 
+    systemd.packages = [ pkgs.mdadm ];
+
+    boot.initrd.availableKernelModules = lib.mkIf (config.boot.initrd.systemd.enable -> cfg.enable) [ "md_mod" "raid0" "raid1" "raid10" "raid456" ];
+
+    boot.initrd.extraUdevRulesCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
+      cp -v ${pkgs.mdadm}/lib/udev/rules.d/*.rules $out/
+    '';
+
+    boot.initrd.systemd = lib.mkIf cfg.enable {
+      contents."/etc/mdadm.conf" = lib.mkIf (cfg.mdadmConf != "") {
+        text = cfg.mdadmConf;
+      };
+
+      packages = [ pkgs.mdadm ];
+      initrdBin = [ pkgs.mdadm ];
+    };
+
+    boot.initrd.services.udev.packages = lib.mkIf cfg.enable [ pkgs.mdadm ];
+  };
 }
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index 01447e6ada879..4ab2578eb81e6 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -65,33 +65,26 @@ in
       };
     };
 
-    boot.initrd.preDeviceCommands =
-      ''
-        echo 600 > /proc/sys/kernel/hung_task_timeout_secs
-      '';
-
-    boot.initrd.postDeviceCommands =
-      ''
-        # Using acpi_pm as a clock source causes the guest clock to
-        # slow down under high host load.  This is usually a bad
-        # thing, but for VM tests it should provide a bit more
-        # determinism (e.g. if the VM runs at lower speed, then
-        # timeouts in the VM should also be delayed).
-        echo acpi_pm > /sys/devices/system/clocksource/clocksource0/current_clocksource
-      '';
-
-    boot.postBootCommands =
-      ''
-        # Panic on out-of-memory conditions rather than letting the
-        # OOM killer randomly get rid of processes, since this leads
-        # to failures that are hard to diagnose.
-        echo 2 > /proc/sys/vm/panic_on_oom
-      '';
+    boot.kernel.sysctl = {
+      "kernel.hung_task_timeout_secs" = 600;
+      # Panic on out-of-memory conditions rather than letting the
+      # OOM killer randomly get rid of processes, since this leads
+      # to failures that are hard to diagnose.
+      "vm.panic_on_oom" = lib.mkDefault 2;
+    };
 
-    # Panic if an error occurs in stage 1 (rather than waiting for
-    # user intervention).
-    boot.kernelParams =
-      [ "console=${qemu-common.qemuSerialDevice}" "panic=1" "boot.panic_on_fail" ];
+    boot.kernelParams = [
+      "console=${qemu-common.qemuSerialDevice}"
+      # Panic if an error occurs in stage 1 (rather than waiting for
+      # user intervention).
+      "panic=1" "boot.panic_on_fail"
+      # Using acpi_pm as a clock source causes the guest clock to
+      # slow down under high host load.  This is usually a bad
+      # thing, but for VM tests it should provide a bit more
+      # determinism (e.g. if the VM runs at lower speed, then
+      # timeouts in the VM should also be delayed).
+      "clock=acpi_pm"
+    ];
 
     # `xwininfo' is used by the test driver to query open windows.
     environment.systemPackages = [ pkgs.xorg.xwininfo ];
@@ -136,6 +129,9 @@ in
     # 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;
+
+    # Squelch warning about unset system.stateVersion
+    system.stateVersion = lib.mkDefault lib.trivial.release;
   };
 
 }
diff --git a/nixos/modules/virtualisation/amazon-ec2-amis.nix b/nixos/modules/virtualisation/amazon-ec2-amis.nix
index 91b5237e3371d..0324c5332cffd 100644
--- a/nixos/modules/virtualisation/amazon-ec2-amis.nix
+++ b/nixos/modules/virtualisation/amazon-ec2-amis.nix
@@ -440,5 +440,53 @@ let self = {
   "21.11".ap-east-1.aarch64-linux.hvm-ebs = "ami-0aa3b50a4f2822a00";
   "21.11".sa-east-1.aarch64-linux.hvm-ebs = "ami-00f68eff453d3fe69";
 
-  latest = self."21.11";
+  # 22.05.342.a634c8f6c1f
+
+  "22.05".eu-west-1.x86_64-linux.hvm-ebs = "ami-00badba5cfa0a0c0d";
+  "22.05".af-south-1.x86_64-linux.hvm-ebs = "ami-0d3a6166c1ea4d7b4";
+  "22.05".ap-east-1.x86_64-linux.hvm-ebs = "ami-06445325c360470d8";
+  "22.05".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-009c422293bcf3721";
+  "22.05".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0bfc0397525a67ed8";
+  "22.05".ap-northeast-3.x86_64-linux.hvm-ebs = "ami-0a1fb4d4e08a6065e";
+  "22.05".ap-south-1.x86_64-linux.hvm-ebs = "ami-07ad258dcc69239d2";
+  "22.05".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0f59f7f33cba8b1a4";
+  "22.05".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-0d1e49fe30aec165d";
+  "22.05".ap-southeast-3.x86_64-linux.hvm-ebs = "ami-0f5cb24a1e3fc62dd";
+  "22.05".ca-central-1.x86_64-linux.hvm-ebs = "ami-0551a595ba7916462";
+  "22.05".eu-central-1.x86_64-linux.hvm-ebs = "ami-0702eee2e75d541d1";
+  "22.05".eu-north-1.x86_64-linux.hvm-ebs = "ami-0fc6838942cb7d9cb";
+  "22.05".eu-south-1.x86_64-linux.hvm-ebs = "ami-0df9463b8965cdb80";
+  "22.05".eu-west-2.x86_64-linux.hvm-ebs = "ami-08f3c1eb533a42ac1";
+  "22.05".eu-west-3.x86_64-linux.hvm-ebs = "ami-04b50c79dc4009c97";
+  "22.05".me-south-1.x86_64-linux.hvm-ebs = "ami-05c52087afab7024d";
+  "22.05".sa-east-1.x86_64-linux.hvm-ebs = "ami-0732aa0f0c28f281b";
+  "22.05".us-east-1.x86_64-linux.hvm-ebs = "ami-0223db08811f6fb2d";
+  "22.05".us-east-2.x86_64-linux.hvm-ebs = "ami-0a743534fa3e51b41";
+  "22.05".us-west-1.x86_64-linux.hvm-ebs = "ami-0d72ab697beab5ea5";
+  "22.05".us-west-2.x86_64-linux.hvm-ebs = "ami-034946f0c47088751";
+
+  "22.05".eu-west-1.aarch64-linux.hvm-ebs = "ami-08114069426233360";
+  "22.05".af-south-1.aarch64-linux.hvm-ebs = "ami-0a9b83913abd61694";
+  "22.05".ap-east-1.aarch64-linux.hvm-ebs = "ami-03966ad4547f532b7";
+  "22.05".ap-northeast-1.aarch64-linux.hvm-ebs = "ami-0eb7e152c8d5aae7d";
+  "22.05".ap-northeast-2.aarch64-linux.hvm-ebs = "ami-08369e00c5528762b";
+  "22.05".ap-northeast-3.aarch64-linux.hvm-ebs = "ami-0fa14b8d48cdd57c3";
+  "22.05".ap-south-1.aarch64-linux.hvm-ebs = "ami-0f2ca3b542ff0913b";
+  "22.05".ap-southeast-1.aarch64-linux.hvm-ebs = "ami-087def0511ef2687d";
+  "22.05".ap-southeast-2.aarch64-linux.hvm-ebs = "ami-0aa90985199011f04";
+  "22.05".ap-southeast-3.aarch64-linux.hvm-ebs = "ami-0c86c52790deefa23";
+  "22.05".ca-central-1.aarch64-linux.hvm-ebs = "ami-06e932cc9c20403e4";
+  "22.05".eu-central-1.aarch64-linux.hvm-ebs = "ami-07680df1026a9b54c";
+  "22.05".eu-north-1.aarch64-linux.hvm-ebs = "ami-0cbe9f2725e4de706";
+  "22.05".eu-south-1.aarch64-linux.hvm-ebs = "ami-01a83c3892925765f";
+  "22.05".eu-west-2.aarch64-linux.hvm-ebs = "ami-049024d086d039b54";
+  "22.05".eu-west-3.aarch64-linux.hvm-ebs = "ami-0c0ebe20ebfc635a1";
+  "22.05".me-south-1.aarch64-linux.hvm-ebs = "ami-0d662fcaac553e945";
+  "22.05".sa-east-1.aarch64-linux.hvm-ebs = "ami-0888c8f703e00fdb8";
+  "22.05".us-east-1.aarch64-linux.hvm-ebs = "ami-03536a13324333073";
+  "22.05".us-east-2.aarch64-linux.hvm-ebs = "ami-067611519fa817aaa";
+  "22.05".us-west-1.aarch64-linux.hvm-ebs = "ami-0f96be48071c13ab2";
+  "22.05".us-west-2.aarch64-linux.hvm-ebs = "ami-084bc5d777585adfb";
+
+  latest = self."22.05";
 }; in self
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index bd7077ff7ea11..12fe6fa444793 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -37,6 +37,9 @@ in
       { assertion = cfg.efi -> cfg.hvm;
         message = "EC2 instances using EFI must be HVM instances.";
       }
+      { assertion = versionOlder config.boot.kernelPackages.kernel.version "5.17";
+        message = "ENA driver fails to build with kernel >= 5.17";
+      }
     ];
 
     boot.growPartition = cfg.hvm;
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
index 4f2f8df90eb8a..9c2adb90bfd21 100644
--- a/nixos/modules/virtualisation/amazon-init.nix
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -11,7 +11,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 pkgs.xz config.system.build.nixos-rebuild]}:$PATH
+    export PATH=${pkgs.lib.makeBinPath [ config.nix.package config.systemd.package 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/anbox.nix b/nixos/modules/virtualisation/anbox.nix
index a4da62eb5f790..c70da573533fa 100644
--- a/nixos/modules/virtualisation/anbox.nix
+++ b/nixos/modules/virtualisation/anbox.nix
@@ -73,9 +73,6 @@ in
 
     environment.systemPackages = with pkgs; [ anbox ];
 
-    boot.kernelModules = [ "ashmem_linux" "binder_linux" ];
-    boot.extraModulePackages = [ kernelPackages.anbox ];
-
     services.udev.extraRules = ''
       KERNEL=="ashmem", NAME="%k", MODE="0666"
       KERNEL=="binder*", NAME="%k", MODE="0666"
diff --git a/nixos/modules/virtualisation/appvm.nix b/nixos/modules/virtualisation/appvm.nix
new file mode 100644
index 0000000000000..24315a85d0edf
--- /dev/null
+++ b/nixos/modules/virtualisation/appvm.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.appvm;
+
+in {
+
+  options = {
+    virtualisation.appvm = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This enables AppVMs and related virtualisation settings.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        description = ''
+          AppVM user login. Currenly only AppVMs are supported for a single user only.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    virtualisation.libvirtd = {
+      enable = true;
+      qemu.verbatimConfig = ''
+        namespaces = []
+        user = "${cfg.user}"
+        group = "users"
+        remember_owner = 0
+      '';
+    };
+
+    users.users."${cfg.user}" = {
+      packages = [ pkgs.appvm ];
+      extraGroups = [ "libvirtd" ];
+    };
+
+  };
+
+}
+
diff --git a/nixos/modules/virtualisation/azure-agent.nix b/nixos/modules/virtualisation/azure-agent.nix
index bd8c7f8c1eea3..e2425b44eac4a 100644
--- a/nixos/modules/virtualisation/azure-agent.nix
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -146,15 +146,11 @@ in
 
     services.logrotate = {
       enable = true;
-      extraConfig = ''
-        /var/log/waagent.log {
-            compress
-            monthly
-            rotate 6
-            notifempty
-            missingok
-        }
-      '';
+      settings."/var/log/waagent.log" = {
+        compress = true;
+        frequency = "monthly";
+        rotate = 6;
+      };
     };
 
     systemd.targets.provisioned = {
diff --git a/nixos/modules/virtualisation/azure-common.nix b/nixos/modules/virtualisation/azure-common.nix
index 8efa177e30d16..dc7853b95032c 100644
--- a/nixos/modules/virtualisation/azure-common.nix
+++ b/nixos/modules/virtualisation/azure-common.nix
@@ -21,7 +21,11 @@ with lib;
   # way to select them anyway.
   boot.loader.grub.configurationLimit = 0;
 
-  fileSystems."/".device = "/dev/disk/by-label/nixos";
+  fileSystems."/" = {
+    device = "/dev/disk/by-label/nixos";
+    fsType = "ext4";
+    autoResize = true;
+  };
 
   # Allow root logins only using the SSH key that the user specified
   # at instance creation time, ping client connections to avoid timeouts
diff --git a/nixos/modules/virtualisation/containerd.nix b/nixos/modules/virtualisation/containerd.nix
index 898a66e7b04e3..ea89a994b172a 100644
--- a/nixos/modules/virtualisation/containerd.nix
+++ b/nixos/modules/virtualisation/containerd.nix
@@ -53,6 +53,7 @@ in
     virtualisation.containerd = {
       args.config = toString containerdConfigChecked;
       settings = {
+        version = 2;
         plugins."io.containerd.grpc.v1.cri" = {
          containerd.snapshotter =
            lib.mkIf config.boot.zfs.enabled (lib.mkOptionDefault "zfs");
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index cf51100015033..38766113f3916 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -71,10 +71,6 @@ in
     package = mkOption {
       type = types.package;
       default = crioPackage;
-      defaultText = literalDocBook ''
-        <literal>pkgs.cri-o</literal> built with
-        <literal>config.${opt.extraPackages}</literal>.
-      '';
       internal = true;
       description = ''
         The final CRI-O package (including extra packages).
diff --git a/nixos/modules/virtualisation/digital-ocean-init.nix b/nixos/modules/virtualisation/digital-ocean-init.nix
index 4339d91de168e..df30104b7d7fd 100644
--- a/nixos/modules/virtualisation/digital-ocean-init.nix
+++ b/nixos/modules/virtualisation/digital-ocean-init.nix
@@ -46,7 +46,7 @@ in {
         RemainAfterExit = true;
       };
       restartIfChanged = false;
-      path = [ pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.systemd config.nix.package config.system.build.nixos-rebuild ];
+      path = [ pkgs.jq pkgs.gnused pkgs.gnugrep config.systemd.package config.nix.package config.system.build.nixos-rebuild ];
       environment = {
         HOME = "/root";
         NIX_PATH = concatStringsSep ":" [
diff --git a/nixos/modules/virtualisation/docker-rootless.nix b/nixos/modules/virtualisation/docker-rootless.nix
index d371f67ecdc84..b814fa1c4358c 100644
--- a/nixos/modules/virtualisation/docker-rootless.nix
+++ b/nixos/modules/virtualisation/docker-rootless.nix
@@ -51,7 +51,6 @@ in
       default = pkgs.docker;
       defaultText = literalExpression "pkgs.docker";
       type = types.package;
-      example = literalExpression "pkgs.docker-edge";
       description = ''
         Docker package to be used in the module.
       '';
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index a69cbe55c7845..c6eca4d6ed584 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -155,7 +155,6 @@ in
       default = pkgs.docker;
       defaultText = literalExpression "pkgs.docker";
       type = types.package;
-      example = literalExpression "pkgs.docker-edge";
       description = ''
         Docker package to be used in the module.
       '';
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index ab87394a30eee..31d18ae734495 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -11,10 +11,9 @@ let
     auth_unix_rw = "polkit"
     ${cfg.extraConfig}
   '';
-  ovmfFilePrefix = if pkgs.stdenv.isAarch64 then "AAVMF" else "OVMF";
   qemuConfigFile = pkgs.writeText "qemu.conf" ''
     ${optionalString cfg.qemu.ovmf.enable ''
-      nvram = [ "/run/libvirt/nix-ovmf/${ovmfFilePrefix}_CODE.fd:/run/libvirt/nix-ovmf/${ovmfFilePrefix}_VARS.fd" ]
+      nvram = [ "/run/libvirt/nix-ovmf/AAVMF_CODE.fd:/run/libvirt/nix-ovmf/AAVMF_VARS.fd", "/run/libvirt/nix-ovmf/OVMF_CODE.fd:/run/libvirt/nix-ovmf/OVMF_VARS.fd" ]
     ''}
     ${optionalString (!cfg.qemu.runAsRoot) ''
       user = "qemu-libvirtd"
@@ -36,13 +35,20 @@ let
         '';
       };
 
+      # mkRemovedOptionModule does not work in submodules, do it manually
       package = mkOption {
-        type = types.package;
-        default = pkgs.OVMF;
-        defaultText = literalExpression "pkgs.OVMF";
-        example = literalExpression "pkgs.OVMFFull";
+        type = types.nullOr types.package;
+        default = null;
+        internal = true;
+      };
+
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [ pkgs.OVMF.fd ];
+        defaultText = literalExpression "[ pkgs.OVMF.fd ]";
+        example = literalExpression "[ pkgs.OVMFFull.fd pkgs.pkgsCross.aarch64-multiplatform.OVMF.fd ]";
         description = ''
-          OVMF package to use.
+          List of OVMF packages to use. Each listed package must contain files names FV/OVMF_CODE.fd and FV/OVMF_VARS.fd or FV/AAVMF_CODE.fd and FV/AAVMF_VARS.fd
         '';
       };
     };
@@ -141,9 +147,9 @@ in
     (mkRenamedOptionModule
       [ "virtualisation" "libvirtd" "qemuOvmf" ]
       [ "virtualisation" "libvirtd" "qemu" "ovmf" "enable" ])
-    (mkRenamedOptionModule
+    (mkRemovedOptionModule
       [ "virtualisation" "libvirtd" "qemuOvmfPackage" ]
-      [ "virtualisation" "libvirtd" "qemu" "ovmf" "package" ])
+      "If this option was set to `foo`, set the option `virtualisation.libvirtd.qemu.ovmf.packages' to `[foo.fd]` instead.")
     (mkRenamedOptionModule
       [ "virtualisation" "libvirtd" "qemuSwtpm" ]
       [ "virtualisation" "libvirtd" "qemu" "swtpm" "enable" ])
@@ -238,12 +244,15 @@ in
 
     assertions = [
       {
-        assertion = config.security.polkit.enable;
-        message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
+        assertion = config.virtualisation.libvirtd.qemu.ovmf.package == null;
+        message = ''
+        The option virtualisation.libvirtd.qemu.ovmf.package is superseded by virtualisation.libvirtd.qemu.ovmf.packages.
+        If this option was set to `foo`, set the option `virtualisation.libvirtd.qemu.ovmf.packages' to `[foo.fd]` instead.
+        '';
       }
       {
-        assertion = builtins.elem "fd" cfg.qemu.ovmf.package.outputs;
-        message = "The option 'virtualisation.libvirtd.qemuOvmfPackage' needs a package that has an 'fd' output.";
+        assertion = config.security.polkit.enable;
+        message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
       }
     ];
 
@@ -303,10 +312,18 @@ in
           ln -s --force ${cfg.qemu.package}/$helper /run/${dirName}/nix-helpers/
         done
 
-        ${optionalString cfg.qemu.ovmf.enable ''
-          ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_CODE.fd /run/${dirName}/nix-ovmf/
-          ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_VARS.fd /run/${dirName}/nix-ovmf/
-        ''}
+        ${optionalString cfg.qemu.ovmf.enable (let
+          ovmfpackage = pkgs.buildEnv {
+            name = "qemu-ovmf";
+            paths = cfg.qemu.ovmf.packages;
+          };
+        in
+          ''
+          ln -s --force ${ovmfpackage}/FV/AAVMF_CODE.fd /run/${dirName}/nix-ovmf/
+          ln -s --force ${ovmfpackage}/FV/OVMF_CODE.fd /run/${dirName}/nix-ovmf/
+          ln -s --force ${ovmfpackage}/FV/AAVMF_VARS.fd /run/${dirName}/nix-ovmf/
+          ln -s --force ${ovmfpackage}/FV/OVMF_VARS.fd /run/${dirName}/nix-ovmf/
+        '')}
       '';
 
       serviceConfig = {
@@ -344,6 +361,10 @@ in
       restartIfChanged = false;
     };
 
+    systemd.services.virtchd = {
+      path = [ pkgs.cloud-hypervisor ];
+    };
+
     systemd.services.libvirt-guests = {
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [ coreutils gawk cfg.package ];
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index 9816cc2332fbd..d3a2e0ed151d3 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -63,18 +63,18 @@ in
         default = {};
         example = literalExpression ''
           {
-            # create /etc/hostname on container creation
+            # create /etc/hostname on container creation. also requires networking.hostName = "" to be set
             "hostname" = {
               enable = true;
               target = "/etc/hostname";
-              template = builtins.writeFile "hostname.tpl" "{{ container.name }}";
+              template = builtins.toFile "hostname.tpl" "{{ container.name }}";
               when = [ "create" ];
             };
             # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied
             "hostname-nix" = {
               enable = true;
               target = "/etc/nixos/hostname.nix";
-              template = builtins.writeFile "hostname-nix.tpl" "{ ... }: { networking.hostName = "{{ container.name }}"; }";
+              template = builtins.toFile "hostname-nix.tpl" "{ ... }: { networking.hostName = \"{{ container.name }}\"; }";
               # copy keeps the file updated when the container is changed
               when = [ "create" "copy" ];
             };
@@ -82,7 +82,7 @@ in
             "configuration-nix" = {
               enable = true;
               target = "/etc/nixos/configuration.nix";
-              template = builtins.writeFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
+              template = builtins.toFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
               when = [ "create" ];
             };
           };
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index 0838a57f0f372..b930151571294 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -4,6 +4,11 @@ with lib;
 
 let
 
+  configurationPrefix = optionalString (versionAtLeast config.system.stateVersion "22.05") "nixos-";
+  configurationDirectoryName = "${configurationPrefix}containers";
+  configurationDirectory = "/etc/${configurationDirectoryName}";
+  stateDirectory = "/var/lib/${configurationPrefix}containers";
+
   # The container's init script, a small wrapper around the regular
   # NixOS stage-2 init script.
   containerInit = (cfg:
@@ -77,7 +82,7 @@ let
   startScript = cfg:
     ''
       mkdir -p -m 0755 "$root/etc" "$root/var/lib"
-      mkdir -p -m 0700 "$root/var/lib/private" "$root/root" /run/containers
+      mkdir -p -m 0700 "$root/var/lib/private" "$root/root" /run/nixos-containers
       if ! [ -e "$root/etc/os-release" ]; then
         touch "$root/etc/os-release"
       fi
@@ -249,11 +254,11 @@ let
 
     SyslogIdentifier = "container %i";
 
-    EnvironmentFile = "-/etc/containers/%i.conf";
+    EnvironmentFile = "-${configurationDirectory}/%i.conf";
 
     Type = "notify";
 
-    RuntimeDirectory = lib.optional cfg.ephemeral "containers/%i";
+    RuntimeDirectory = lib.optional cfg.ephemeral "${configurationDirectoryName}/%i";
 
     # Note that on reboot, systemd-nspawn returns 133, so this
     # unit will be restarted. On poweroff, it returns 0, so the
@@ -279,7 +284,7 @@ let
     DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
   };
 
-  system = config.nixpkgs.localSystem.system;
+  inherit (config.nixpkgs) localSystem;
   kernelVersion = config.boot.kernelPackages.kernel.version;
 
   bindMountOpts = { name, ... }: {
@@ -473,12 +478,12 @@ in
               type = lib.mkOptionType {
                 name = "Toplevel NixOS config";
                 merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
-                  inherit system;
                   modules =
                     let
                       extraConfig = {
                         _file = "module at ${__curPos.file}:${toString __curPos.line}";
                         config = {
+                          nixpkgs = { inherit localSystem; };
                           boot.isContainer = true;
                           networking.hostName = mkDefault name;
                           networking.useDHCP = false;
@@ -737,15 +742,21 @@ in
 
   config = mkIf (config.boot.enableContainers) (let
 
+    warnings = flatten [
+      (optional (config.virtualisation.containers.enable && versionOlder config.system.stateVersion "22.05") ''
+        Enabling both boot.enableContainers & virtualisation.containers on system.stateVersion < 22.05 is unsupported.
+      '')
+    ];
+
     unit = {
       description = "Container '%i'";
 
-      unitConfig.RequiresMountsFor = "/var/lib/containers/%i";
+      unitConfig.RequiresMountsFor = "${stateDirectory}/%i";
 
       path = [ pkgs.iproute2 ];
 
       environment = {
-        root = "/var/lib/containers/%i";
+        root = "${stateDirectory}/%i";
         INSTANCE = "%i";
       };
 
@@ -782,8 +793,8 @@ in
             script = startScript containerConfig;
             postStart = postStartScript containerConfig;
             serviceConfig = serviceDirectives containerConfig;
-            unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "/var/lib/containers/%i";
-            environment.root = if containerConfig.ephemeral then "/run/containers/%i" else "/var/lib/containers/%i";
+            unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "${stateDirectory}/%i";
+            environment.root = if containerConfig.ephemeral then "/run/nixos-containers/%i" else "${stateDirectory}/%i";
           } // (
           if containerConfig.autoStart then
             {
@@ -792,7 +803,7 @@ in
               after = [ "network.target" ];
               restartTriggers = [
                 containerConfig.path
-                config.environment.etc."containers/${name}.conf".source
+                config.environment.etc."${configurationDirectoryName}/${name}.conf".source
               ];
               restartIfChanged = true;
             }
@@ -800,12 +811,12 @@ in
       )) config.containers)
     ));
 
-    # Generate a configuration file in /etc/containers for each
+    # Generate a configuration file in /etc/nixos-containers for each
     # container so that container@.target can get the container
     # configuration.
     environment.etc =
       let mkPortStr = p: p.protocol + ":" + (toString p.hostPort) + ":" + (if p.containerPort == null then toString p.hostPort else toString p.containerPort);
-      in mapAttrs' (name: cfg: nameValuePair "containers/${name}.conf"
+      in mapAttrs' (name: cfg: nameValuePair "${configurationDirectoryName}/${name}.conf"
       { text =
           ''
             SYSTEM_PATH=${cfg.path}
@@ -854,7 +865,11 @@ in
       ENV{INTERFACE}=="v[eb]-*", ENV{NM_UNMANAGED}="1"
     '';
 
-    environment.systemPackages = [ pkgs.nixos-container ];
+    environment.systemPackages = [
+      (pkgs.nixos-container.override {
+        inherit stateDirectory configurationDirectory;
+      })
+    ];
 
     boot.kernelModules = [
       "bridge"
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 5af9baff8bc1b..81cdf1dd72b46 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -14,19 +14,21 @@ let
 
         image = mkOption {
           type = with types; str;
-          description = "OCI image to run.";
+          description = lib.mdDoc "OCI image to run.";
           example = "library/hello-world";
         };
 
         imageFile = mkOption {
           type = with types; nullOr package;
           default = null;
-          description = ''
-            Path to an image file to load instead of pulling from a registry.
-            If defined, do not pull from registry.
-
-            You still need to set the <literal>image</literal> attribute, as it
-            will be used as the image name for docker to start a container.
+          description = lib.mdDoc ''
+            Path to an image file to load before running the image. This can
+            be used to bypass pulling the image from the registry.
+
+            The `image` attribute must match the name and
+            tag of the image contained in this file, as they will be used to
+            run the container with that image. If they do not match, the
+            image will be pulled from the registry as usual.
           '';
           example = literalExpression "pkgs.dockerTools.buildImage {...};";
         };
@@ -36,20 +38,20 @@ let
           username = mkOption {
             type = with types; nullOr str;
             default = null;
-            description = "Username for login.";
+            description = lib.mdDoc "Username for login.";
           };
 
           passwordFile = mkOption {
             type = with types; nullOr str;
             default = null;
-            description = "Path to file containing password.";
+            description = lib.mdDoc "Path to file containing password.";
             example = "/etc/nixos/dockerhub-password.txt";
           };
 
           registry = mkOption {
             type = with types; nullOr str;
             default = null;
-            description = "Registry where to login to.";
+            description = lib.mdDoc "Registry where to login to.";
             example = "https://docker.pkg.github.com";
           };
 
@@ -58,7 +60,7 @@ let
         cmd = mkOption {
           type =  with types; listOf str;
           default = [];
-          description = "Commandline arguments to pass to the image's entrypoint.";
+          description = lib.mdDoc "Commandline arguments to pass to the image's entrypoint.";
           example = literalExpression ''
             ["--port=9000"]
           '';
@@ -66,7 +68,7 @@ let
 
         entrypoint = mkOption {
           type = with types; nullOr str;
-          description = "Override the default entrypoint of the image.";
+          description = lib.mdDoc "Override the default entrypoint of the image.";
           default = null;
           example = "/bin/my-app";
         };
@@ -74,7 +76,7 @@ let
         environment = mkOption {
           type = with types; attrsOf str;
           default = {};
-          description = "Environment variables to set for this container.";
+          description = lib.mdDoc "Environment variables to set for this container.";
           example = literalExpression ''
             {
               DATABASE_HOST = "db.example.com";
@@ -86,7 +88,7 @@ let
         environmentFiles = mkOption {
           type = with types; listOf path;
           default = [];
-          description = "Environment files for this container.";
+          description = lib.mdDoc "Environment files for this container.";
           example = literalExpression ''
             [
               /path/to/.env
@@ -98,15 +100,15 @@ let
         log-driver = mkOption {
           type = types.str;
           default = "journald";
-          description = ''
+          description = lib.mdDoc ''
             Logging driver for the container.  The default of
-            <literal>"journald"</literal> means that the container's logs will be
+            `"journald"` means that the container's logs will be
             handled as part of the systemd unit.
 
             For more details and a full list of logging drivers, refer to respective backends documentation.
 
             For Docker:
-            <link xlink:href="https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver">Docker engine documentation</link>
+            [Docker engine documentation](https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver)
 
             For Podman:
             Refer to the docker-run(1) man page.
@@ -116,49 +118,27 @@ let
         ports = mkOption {
           type = with types; listOf str;
           default = [];
-          description = ''
+          description = lib.mdDoc ''
             Network ports to publish from the container to the outer host.
 
             Valid formats:
+            - `<ip>:<hostPort>:<containerPort>`
+            - `<ip>::<containerPort>`
+            - `<hostPort>:<containerPort>`
+            - `<containerPort>`
 
-            <itemizedlist>
-              <listitem>
-                <para>
-                  <literal>&lt;ip&gt;:&lt;hostPort&gt;:&lt;containerPort&gt;</literal>
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <literal>&lt;ip&gt;::&lt;containerPort&gt;</literal>
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <literal>&lt;hostPort&gt;:&lt;containerPort&gt;</literal>
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <literal>&lt;containerPort&gt;</literal>
-                </para>
-              </listitem>
-            </itemizedlist>
-
-            Both <literal>hostPort</literal> and
-            <literal>containerPort</literal> can be specified as a range of
+            Both `hostPort` and `containerPort` can be specified as a range of
             ports.  When specifying ranges for both, the number of container
             ports in the range must match the number of host ports in the
-            range.  Example: <literal>1234-1236:1234-1236/tcp</literal>
+            range.  Example: `1234-1236:1234-1236/tcp`
 
-            When specifying a range for <literal>hostPort</literal> only, the
-            <literal>containerPort</literal> must <emphasis>not</emphasis> be a
-            range.  In this case, the container port is published somewhere
-            within the specified <literal>hostPort</literal> range.  Example:
-            <literal>1234-1236:1234/tcp</literal>
+            When specifying a range for `hostPort` only, the `containerPort`
+            must *not* be a range.  In this case, the container port is published
+            somewhere within the specified `hostPort` range.
+            Example: `1234-1236:1234/tcp`
 
             Refer to the
-            <link xlink:href="https://docs.docker.com/engine/reference/run/#expose-incoming-ports">
-            Docker engine documentation</link> for full details.
+            [Docker engine documentation](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) for full details.
           '';
           example = literalExpression ''
             [
@@ -170,7 +150,7 @@ let
         user = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = ''
+          description = lib.mdDoc ''
             Override the username or UID (and optionally groupname or GID) used
             in the container.
           '';
@@ -180,16 +160,15 @@ let
         volumes = mkOption {
           type = with types; listOf str;
           default = [];
-          description = ''
+          description = lib.mdDoc ''
             List of volumes to attach to this container.
 
-            Note that this is a list of <literal>"src:dst"</literal> strings to
-            allow for <literal>src</literal> to refer to
-            <literal>/nix/store</literal> paths, which would be difficult with an
-            attribute set.  There are also a variety of mount options available
-            as a third field; please refer to the
-            <link xlink:href="https://docs.docker.com/engine/reference/run/#volume-shared-filesystems">
-            docker engine documentation</link> for details.
+            Note that this is a list of `"src:dst"` strings to
+            allow for `src` to refer to `/nix/store` paths, which
+            would be difficult with an attribute set.  There are
+            also a variety of mount options available as a third
+            field; please refer to the
+            [docker engine documentation](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems) for details.
           '';
           example = literalExpression ''
             [
@@ -202,17 +181,17 @@ let
         workdir = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Override the default working directory for the container.";
+          description = lib.mdDoc "Override the default working directory for the container.";
           example = "/var/lib/hello_world";
         };
 
         dependsOn = mkOption {
           type = with types; listOf str;
           default = [];
-          description = ''
+          description = lib.mdDoc ''
             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.containers</literal>.
+            Use the same name as the attribute under `virtualisation.oci-containers.containers`.
           '';
           example = literalExpression ''
             virtualisation.oci-containers.containers = {
@@ -227,7 +206,7 @@ let
         extraOptions = mkOption {
           type = with types; listOf str;
           default = [];
-          description = "Extra options for <command>${defaultBackend} run</command>.";
+          description = lib.mdDoc "Extra options for {command}`${defaultBackend} run`.";
           example = literalExpression ''
             ["--network=host"]
           '';
@@ -236,7 +215,7 @@ let
         autoStart = mkOption {
           type = types.bool;
           default = true;
-          description = ''
+          description = lib.mdDoc ''
             When enabled, the container is automatically started on boot.
             If this option is set to false, the container has to be started on-demand via its service.
           '';
@@ -336,18 +315,14 @@ in {
 
     backend = mkOption {
       type = types.enum [ "podman" "docker" ];
-      default =
-        # TODO: Once https://github.com/NixOS/nixpkgs/issues/77925 is resolved default to podman
-        # if versionAtLeast config.system.stateVersion "20.09" then "podman"
-        # else "docker";
-        "docker";
-      description = "The underlying Docker implementation to use.";
+      default = if versionAtLeast config.system.stateVersion "22.05" then "podman" else "docker";
+      description = lib.mdDoc "The underlying Docker implementation to use.";
     };
 
     containers = mkOption {
       default = {};
       type = types.attrsOf (types.submodule containerOptions);
-      description = "OCI (Docker) containers to run as systemd services.";
+      description = lib.mdDoc "OCI (Docker) containers to run as systemd services.";
     };
 
   };
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
index d01e0f23aba17..af4f574661091 100644
--- a/nixos/modules/virtualisation/openstack-config.nix
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -1,8 +1,11 @@
-{ pkgs, lib, ... }:
+{ config, pkgs, lib, ... }:
 
-with lib;
+# image metadata:
+# hw_firmware_type=uefi
 
 let
+  inherit (lib) mkIf mkDefault;
+  cfg = config.openstack;
   metadataFetcher = import ./openstack-metadata-fetcher.nix {
     targetRoot = "/";
     wgetExtraOptions = "--retry-connrefused";
@@ -11,23 +14,47 @@ in
 {
   imports = [
     ../profiles/qemu-guest.nix
+
+    # Note: While we do use the headless profile, we also explicitly
+    # turn on the serial console on tty1 below.
+    # Note that I could not find any documentation indicating tty1 was
+    # the correct choice. I picked tty1 because that is what one
+    # particular host was using.
     ../profiles/headless.nix
+
     # The Openstack Metadata service exposes data on an EC2 API also.
     ./ec2-data.nix
     ./amazon-init.nix
   ];
 
   config = {
-    fileSystems."/" = {
+    fileSystems."/" = mkIf (!cfg.zfs.enable) {
       device = "/dev/disk/by-label/nixos";
       fsType = "ext4";
       autoResize = true;
     };
 
+    fileSystems."/boot" = mkIf (cfg.efi || cfg.zfs.enable) {
+      # The ZFS image uses a partition labeled ESP whether or not we're
+      # booting with EFI.
+      device = "/dev/disk/by-label/ESP";
+      fsType = "vfat";
+    };
+
     boot.growPartition = true;
-    boot.kernelParams = [ "console=ttyS0" ];
-    boot.loader.grub.device = "/dev/vda";
-    boot.loader.timeout = 0;
+    boot.kernelParams = [ "console=tty1" ];
+    boot.loader.grub.device = if (!cfg.efi) then "/dev/vda" else "nodev";
+    boot.loader.grub.efiSupport = cfg.efi;
+    boot.loader.grub.efiInstallAsRemovable = cfg.efi;
+    boot.loader.timeout = 1;
+    boot.loader.grub.extraConfig = ''
+      serial --unit=1 --speed=115200 --word=8 --parity=no --stop=1
+      terminal_output console serial
+      terminal_input console serial
+    '';
+
+    services.zfs.expandOnBoot = mkIf cfg.zfs.enable (lib.mkDefault "all");
+    boot.zfs.devNodes = mkIf cfg.zfs.enable "/dev/";
 
     # Allow root logins
     services.openssh = {
@@ -36,6 +63,11 @@ in
       passwordAuthentication = mkDefault false;
     };
 
+    users.users.root.initialPassword = "foobar";
+
+    # Enable the serial console on tty1
+    systemd.services."serial-getty@tty1".enable = true;
+
     # Force getting the hostname from Openstack metadata.
     networking.hostName = mkDefault "";
 
@@ -43,7 +75,7 @@ in
       path = [ pkgs.wget ];
       description = "Fetch Metadata on startup";
       wantedBy = [ "multi-user.target" ];
-      before = [ "apply-ec2-data.service" "amazon-init.service"];
+      before = [ "apply-ec2-data.service" "amazon-init.service" ];
       wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       script = metadataFetcher;
diff --git a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
index 133cd4c0e9f91..d62428b47a449 100644
--- a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
+++ b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
@@ -14,8 +14,9 @@
     wget ${wgetExtraOptions} "$@"
   }
 
-  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
-  (umask 077 && wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/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
+  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path || true
+  # When no user-data is provided, the OpenStack metadata server doesn't expose the user-data route.
+  (umask 077 && wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data || rm -f "$metaDir/user-data")
+  wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname || true
+  wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key || true
 ''
diff --git a/nixos/modules/virtualisation/openstack-options.nix b/nixos/modules/virtualisation/openstack-options.nix
new file mode 100644
index 0000000000000..cbc779f27c8f8
--- /dev/null
+++ b/nixos/modules/virtualisation/openstack-options.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) literalExpression types;
+in
+{
+  options = {
+    openstack = {
+      zfs = {
+        enable = lib.mkOption {
+          default = false;
+          internal = true;
+          description = ''
+            Whether the OpenStack instance uses a ZFS root.
+          '';
+        };
+
+        datasets = lib.mkOption {
+          description = ''
+            Datasets to create under the `tank` and `boot` zpools.
+
+            **NOTE:** This option is used only at image creation time, and
+            does not attempt to declaratively create or manage datasets
+            on an existing system.
+          '';
+
+          default = { };
+
+          type = types.attrsOf (types.submodule {
+            options = {
+              mount = lib.mkOption {
+                description = "Where to mount this dataset.";
+                type = types.nullOr types.string;
+                default = null;
+              };
+
+              properties = lib.mkOption {
+                description = "Properties to set on this dataset.";
+                type = types.attrsOf types.string;
+                default = { };
+              };
+            };
+          });
+        };
+      };
+
+      efi = lib.mkOption {
+        default = pkgs.stdenv.hostPlatform.isAarch64;
+        defaultText = literalExpression "pkgs.stdenv.hostPlatform.isAarch64";
+        internal = true;
+        description = ''
+          Whether the instance is using EFI.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf config.openstack.zfs.enable {
+    networking.hostId = lib.mkDefault "00000000";
+
+    fileSystems =
+      let
+        mountable = lib.filterAttrs (_: value: ((value.mount or null) != null)) config.openstack.zfs.datasets;
+      in
+      lib.mapAttrs'
+        (dataset: opts: lib.nameValuePair opts.mount {
+          device = dataset;
+          fsType = "zfs";
+        })
+        mountable;
+  };
+}
diff --git a/nixos/modules/virtualisation/parallels-guest.nix b/nixos/modules/virtualisation/parallels-guest.nix
index d950cecff6f0c..53ad2ac708b80 100644
--- a/nixos/modules/virtualisation/parallels-guest.nix
+++ b/nixos/modules/virtualisation/parallels-guest.nix
@@ -34,7 +34,8 @@ in
       package = mkOption {
         type = types.nullOr types.package;
         default = config.boot.kernelPackages.prl-tools;
-        defaultText = literalExpression "config.boot.kernelPackages.prl-tools";
+        defaultText = "config.boot.kernelPackages.prl-tools";
+        example = literalExpression "config.boot.kernelPackages.prl-tools";
         description = ''
           Defines which package to use for prl-tools. Override to change the version.
         '';
@@ -44,27 +45,6 @@ in
   };
 
   config = mkIf config.hardware.parallels.enable {
-    services.xserver = {
-      drivers = singleton
-        { name = "prlvideo"; modules = [ prl-tools ]; };
-
-      screenSection = ''
-        Option "NoMTRR"
-      '';
-
-      config = ''
-        Section "InputClass"
-          Identifier "prlmouse"
-          MatchIsPointer "on"
-          MatchTag "prlmouse"
-          Driver "prlmouse"
-        EndSection
-      '';
-    };
-
-    hardware.opengl.package = prl-tools;
-    hardware.opengl.package32 = pkgs.pkgsi686Linux.linuxPackages.prl-tools.override { libsOnly = true; kernel = null; };
-    hardware.opengl.setLdLibraryPath = true;
 
     services.udev.packages = [ prl-tools ];
 
@@ -72,37 +52,44 @@ in
 
     boot.extraModulePackages = [ prl-tools ];
 
-    boot.kernelModules = [ "prl_tg" "prl_eth" "prl_fs" "prl_fs_freeze" ];
+    boot.kernelModules = [ "prl_fs" "prl_fs_freeze" "prl_tg" ]
+      ++ optional (pkgs.stdenv.hostPlatform.system == "aarch64-linux") "prl_notifier";
 
     services.timesyncd.enable = false;
 
     systemd.services.prltoolsd = {
-      description = "Parallels Tools' service";
+      description = "Parallels Tools Service";
       wantedBy = [ "multi-user.target" ];
+      path = [ prl-tools ];
       serviceConfig = {
         ExecStart = "${prl-tools}/bin/prltoolsd -f";
         PIDFile = "/var/run/prltoolsd.pid";
+        WorkingDirectory = "${prl-tools}/bin";
       };
     };
 
     systemd.services.prlfsmountd = mkIf config.hardware.parallels.autoMountShares {
-      description = "Parallels Shared Folders Daemon";
+      description = "Parallels Guest File System Sharing Tool";
       wantedBy = [ "multi-user.target" ];
+      path = [ prl-tools ];
       serviceConfig = rec {
         ExecStart = "${prl-tools}/sbin/prlfsmountd ${PIDFile}";
         ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /media";
         ExecStopPost = "${prl-tools}/sbin/prlfsmountd -u";
         PIDFile = "/run/prlfsmountd.pid";
+        WorkingDirectory = "${prl-tools}/bin";
       };
     };
 
     systemd.services.prlshprint = {
-      description = "Parallels Shared Printer Tool";
+      description = "Parallels Printing Tool";
       wantedBy = [ "multi-user.target" ];
       bindsTo = [ "cups.service" ];
+      path = [ prl-tools ];
       serviceConfig = {
         Type = "forking";
         ExecStart = "${prl-tools}/bin/prlshprint";
+        WorkingDirectory = "${prl-tools}/bin";
       };
     };
 
@@ -110,43 +97,47 @@ in
       prlcc = {
         description = "Parallels Control Center";
         wantedBy = [ "graphical-session.target" ];
+        path = [ prl-tools ];
         serviceConfig = {
           ExecStart = "${prl-tools}/bin/prlcc";
+          WorkingDirectory = "${prl-tools}/bin";
         };
       };
       prldnd = {
-        description = "Parallels Control Center";
+        description = "Parallels Drag And Drop Tool";
         wantedBy = [ "graphical-session.target" ];
+        path = [ prl-tools ];
         serviceConfig = {
           ExecStart = "${prl-tools}/bin/prldnd";
-        };
-      };
-      prl_wmouse_d  = {
-        description = "Parallels Walking Mouse Daemon";
-        wantedBy = [ "graphical-session.target" ];
-        serviceConfig = {
-          ExecStart = "${prl-tools}/bin/prl_wmouse_d";
+          WorkingDirectory = "${prl-tools}/bin";
         };
       };
       prlcp = {
-        description = "Parallels CopyPaste Tool";
+        description = "Parallels Copy Paste Tool";
         wantedBy = [ "graphical-session.target" ];
+        path = [ prl-tools ];
         serviceConfig = {
           ExecStart = "${prl-tools}/bin/prlcp";
+          Restart = "always";
+          WorkingDirectory = "${prl-tools}/bin";
         };
       };
       prlsga = {
         description = "Parallels Shared Guest Applications Tool";
         wantedBy = [ "graphical-session.target" ];
+        path = [ prl-tools ];
         serviceConfig = {
           ExecStart = "${prl-tools}/bin/prlsga";
+          WorkingDirectory = "${prl-tools}/bin";
         };
       };
       prlshprof = {
         description = "Parallels Shared Profile Tool";
         wantedBy = [ "graphical-session.target" ];
+        path = [ prl-tools ];
         serviceConfig = {
           ExecStart = "${prl-tools}/bin/prlshprof";
+          WorkingDirectory = "${prl-tools}/bin";
         };
       };
     };
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
index 94fd727a4b564..361caeff70bef 100644
--- a/nixos/modules/virtualisation/podman/default.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -6,7 +6,10 @@ let
 
   inherit (lib) mkOption types;
 
-  podmanPackage = (pkgs.podman.override { inherit (cfg) extraPackages; });
+  podmanPackage = (pkgs.podman.override {
+    extraPackages = cfg.extraPackages
+      ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
+  });
 
   # Provides a fake "docker" binary mapping to podman
   dockerCompat = pkgs.runCommand "${podmanPackage.pname}-docker-compat-${podmanPackage.version}" {
@@ -150,6 +153,12 @@ in
       systemd.sockets.podman.wantedBy = [ "sockets.target" ];
       systemd.sockets.podman.socketConfig.SocketGroup = "podman";
 
+      systemd.user.services.podman.serviceConfig = {
+        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
+      };
+
+      systemd.user.sockets.podman.wantedBy = [ "sockets.target" ];
+
       systemd.tmpfiles.packages = [
         # The /run/podman rule interferes with our podman group, so we remove
         # it and let the systemd socket logic take care of it.
diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix
index c537d5aed4471..e07d1d1eb3fc6 100644
--- a/nixos/modules/virtualisation/proxmox-image.nix
+++ b/nixos/modules/virtualisation/proxmox-image.nix
@@ -127,16 +127,26 @@ with lib;
       name = "proxmox-${cfg.filenameSuffix}";
       postVM = let
         # Build qemu with PVE's patch that adds support for the VMA format
-        vma = pkgs.qemu_kvm.overrideAttrs ( super: {
+        vma = pkgs.qemu_kvm.overrideAttrs ( super: rec {
+
+          # proxmox's VMA patch doesn't work with qemu 7.0 yet
+          version = "6.2.0";
+          src = pkgs.fetchurl {
+            url= "https://download.qemu.org/qemu-${version}.tar.xz";
+            hash = "sha256-aOFdjkWsVjJuC5pK+otJo9/oq6NIgiHQmMhGmLymW0U=";
+          };
+
           patches = let
-            rev = "cc707c362ea5c8d832aac270d1ffa7ac66a8908f";
-            path = "debian/patches/pve/0025-PVE-Backup-add-vma-backup-format-code.patch";
+            rev = "b37b17c286da3d32945fbee8ee4fd97a418a50db";
+            path = "debian/patches/pve/0026-PVE-Backup-add-vma-backup-format-code.patch";
             vma-patch = pkgs.fetchpatch {
-              url = "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;hb=${rev};f=${path}";
-              sha256 = "1z467xnmfmry3pjy7p34psd5xdil9x0apnbvfz8qbj0bf9fgc8zf";
+              url = "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;h=${rev};f=${path}";
+              hash = "sha256-siuDWDUnM9Zq0/L2Faww3ELAOUHhVIHu5RAQn6L4Atc=";
             };
-          in super.patches ++ [ vma-patch ];
+          in [ vma-patch ];
+
           buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
+
         });
       in
       ''
diff --git a/nixos/modules/virtualisation/proxmox-lxc.nix b/nixos/modules/virtualisation/proxmox-lxc.nix
new file mode 100644
index 0000000000000..9b9f99e5b8172
--- /dev/null
+++ b/nixos/modules/virtualisation/proxmox-lxc.nix
@@ -0,0 +1,75 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options.proxmoxLXC = {
+    privileged = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable privileged mounts
+      '';
+    };
+    manageNetwork = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to manage network interfaces through nix options
+        When false, systemd-networkd is enabled to accept network
+        configuration from proxmox.
+      '';
+    };
+    manageHostName = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to manage hostname through nix options
+        When false, the hostname is picked up from /etc/hostname
+        populated by proxmox.
+      '';
+    };
+  };
+
+  config =
+    let
+      cfg = config.proxmoxLXC;
+    in
+    {
+      system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
+        storeContents = [{
+          object = config.system.build.toplevel;
+          symlink = "none";
+        }];
+
+        contents = [{
+          source = config.system.build.toplevel + "/init";
+          target = "/sbin/init";
+        }];
+
+        extraCommands = "mkdir -p root etc/systemd/network";
+      };
+
+      boot = {
+        isContainer = true;
+        loader.initScript.enable = true;
+      };
+
+      networking = mkIf (!cfg.manageNetwork) {
+        useDHCP = false;
+        useHostResolvConf = false;
+        useNetworkd = true;
+        # pick up hostname from /etc/hostname generated by proxmox
+        hostName = mkIf (!cfg.manageHostName) (mkForce "");
+      };
+
+      services.openssh = {
+        enable = mkDefault true;
+        startWhenNeeded = mkDefault true;
+      };
+
+      systemd.mounts = mkIf (!cfg.privileged)
+        [{ where = "/sys/kernel/debug"; enable = false; }];
+
+    };
+}
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 5143893589470..e87f540fd57cb 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -684,6 +684,21 @@ in
           '';
       };
 
+    virtualisation.useDefaultFilesystems =
+      mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            If enabled, the boot disk of the virtual machine will be
+            formatted and mounted with the default filesystems for
+            testing. Swap devices and LUKS will be disabled.
+
+            If disabled, a root filesystem has to be specified and
+            formatted (for example in the initial ramdisk).
+          '';
+      };
+
     virtualisation.efiVars =
       mkOption {
         type = types.str;
@@ -754,13 +769,13 @@ in
     );
     boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
 
-    boot.initrd.extraUtilsCommands =
+    boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
       ''
         # We need mke2fs in the initrd.
         copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
       '';
 
-    boot.initrd.postDeviceCommands =
+    boot.initrd.postDeviceCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
       ''
         # If the disk image appears to be empty, run mke2fs to
         # initialise.
@@ -770,7 +785,7 @@ in
         fi
       '';
 
-    boot.initrd.postMountCommands =
+    boot.initrd.postMountCommands = lib.mkIf (!config.boot.initrd.systemd.enable)
       ''
         # Mark this as a NixOS machine.
         mkdir -p $targetRoot/etc
@@ -789,6 +804,11 @@ in
         ''}
       '';
 
+    systemd.tmpfiles.rules = lib.mkIf config.boot.initrd.systemd.enable [
+      "f /etc/NIXOS 0644 root root -"
+      "d /boot 0644 root root -"
+    ];
+
     # After booting, register the closure of the paths in
     # `virtualisation.additionalPaths' in the Nix database in the VM.  This
     # allows Nix operations to work in the VM.  The path to the
@@ -796,7 +816,7 @@ in
     # allow `system.build.toplevel' to be included.  (If we had a direct
     # reference to ${regInfo} here, then we would get a cyclic
     # dependency.)
-    boot.postBootCommands =
+    boot.postBootCommands = lib.mkIf config.nix.enable
       ''
         if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
           ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
@@ -853,8 +873,12 @@ in
       (mkIf (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [
         "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet"
       ])
-      (mkIf (!cfg.useBootLoader) [
-        "-kernel ${config.system.build.toplevel}/kernel"
+      (let
+        alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
+        # Replace all non-alphanumeric characters with underscores
+        sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s);
+      in mkIf (!cfg.useBootLoader) [
+        "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}"
         "-initrd ${config.system.build.toplevel}/initrd"
         ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
       ])
@@ -921,47 +945,77 @@ in
         };
     in
       mkVMOverride (cfg.fileSystems //
-      {
+      optionalAttrs cfg.useDefaultFilesystems {
         "/".device = cfg.bootDevice;
-
-        "/tmp" = mkIf config.boot.tmpOnTmpfs
-          { device = "tmpfs";
-            fsType = "tmpfs";
-            neededForBoot = true;
-            # Sync with systemd's tmp.mount;
-            options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
-          };
-
-        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" =
-          mkIf cfg.useNixStoreImage
-            { device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
-              neededForBoot = true;
-              options = [ "ro" ];
-            };
-
-        "/nix/.rw-store" = mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs)
-          { fsType = "tmpfs";
-            options = [ "mode=0755" ];
-            neededForBoot = true;
-          };
-
-        "/boot" = mkIf cfg.useBootLoader
-          # see note [Disk layout with `useBootLoader`]
-          { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
-            fsType = "vfat";
-            noCheck = true; # fsck fails on a r/o filesystem
-          };
+        "/".fsType = "ext4";
+        "/".autoFormat = true;
+      } //
+      optionalAttrs config.boot.tmpOnTmpfs {
+        "/tmp" = {
+          device = "tmpfs";
+          fsType = "tmpfs";
+          neededForBoot = true;
+          # Sync with systemd's tmp.mount;
+          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
+        };
+      } //
+      optionalAttrs cfg.useNixStoreImage {
+        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = {
+          device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
+          neededForBoot = true;
+          options = [ "ro" ];
+        };
+      } //
+      optionalAttrs (cfg.writableStore && cfg.writableStoreUseTmpfs) {
+        "/nix/.rw-store" = {
+          fsType = "tmpfs";
+          options = [ "mode=0755" ];
+          neededForBoot = true;
+        };
+      } //
+      optionalAttrs cfg.useBootLoader {
+        # see note [Disk layout with `useBootLoader`]
+        "/boot" = {
+          device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
+          fsType = "vfat";
+          noCheck = true; # fsck fails on a r/o filesystem
+        };
       } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories);
 
-    swapDevices = mkVMOverride [ ];
-    boot.initrd.luks.devices = mkVMOverride {};
+    boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) {
+      mounts = [{
+        where = "/sysroot/nix/store";
+        what = "overlay";
+        type = "overlay";
+        options = "lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/store,workdir=/sysroot/nix/.rw-store/work";
+        wantedBy = ["local-fs.target"];
+        before = ["local-fs.target"];
+        requires = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"];
+        after = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"];
+        unitConfig.IgnoreOnIsolate = true;
+      }];
+      services.rw-store = {
+        after = ["sysroot-nix-.rw\\x2dstore.mount"];
+        unitConfig.DefaultDependencies = false;
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "/bin/mkdir -p 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store";
+        };
+      };
+    };
+
+    swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ];
+    boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) {};
 
     # Don't run ntpd in the guest.  It should get the correct time from KVM.
     services.timesyncd.enable = false;
 
     services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
 
-    system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; }
+    system.build.vm = pkgs.runCommand "nixos-vm" {
+      preferLocalBuild = true;
+      meta.mainProgram = "run-${config.system.name}-vm";
+    }
       ''
         mkdir -p $out/bin
         ln -s ${config.system.build.toplevel} $out/system
diff --git a/nixos/modules/virtualisation/railcar.nix b/nixos/modules/virtualisation/railcar.nix
deleted file mode 100644
index e719e25650d37..0000000000000
--- a/nixos/modules/virtualisation/railcar.nix
+++ /dev/null
@@ -1,124 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.railcar;
-  generateUnit = name: containerConfig:
-    let
-      container = pkgs.ociTools.buildContainer {
-        args = [
-          (pkgs.writeShellScript "run.sh" containerConfig.cmd).outPath
-        ];
-      };
-    in
-      nameValuePair "railcar-${name}" {
-        enable = true;
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-            ExecStart = ''
-              ${cfg.package}/bin/railcar -r ${cfg.stateDir} run ${name} -b ${container}
-            '';
-            Type = containerConfig.runType;
-          };
-      };
-  mount = with types; (submodule {
-    options = {
-      type = mkOption {
-        type = str;
-        default = "none";
-        description = ''
-          The type of the filesystem to be mounted.
-          Linux: filesystem types supported by the kernel as listed in
-          `/proc/filesystems` (e.g., "minix", "ext2", "ext3", "jfs", "xfs",
-          "reiserfs", "msdos", "proc", "nfs", "iso9660"). For bind mounts
-          (when options include either bind or rbind), the type is a dummy,
-          often "none" (not listed in /proc/filesystems).
-        '';
-      };
-      source = mkOption {
-        type = str;
-        description = "Source for the in-container mount";
-      };
-      options = mkOption {
-        type = listOf str;
-        default = [ "bind" ];
-        description = ''
-          Mount options of the filesystem to be used.
-
-          Support options are listed in the mount(8) man page. Note that
-          both filesystem-independent and filesystem-specific options
-          are listed.
-        '';
-      };
-    };
-  });
-in
-{
-  options.services.railcar = {
-    enable = mkEnableOption "railcar";
-
-    containers = mkOption {
-      default = {};
-      description = "Declarative container configuration";
-      type = with types; attrsOf (submodule ({ name, config, ... }: {
-        options = {
-          cmd = mkOption {
-            type = types.lines;
-            description = "Command or script to run inside the container";
-          };
-
-          mounts = mkOption {
-            type = with types; attrsOf mount;
-            default = {};
-            description = ''
-              A set of mounts inside the container.
-
-              The defaults have been chosen for simple bindmounts, meaning
-              that you only need to provide the "source" parameter.
-            '';
-            example = { "/data" = { source = "/var/lib/data"; }; };
-          };
-
-          runType = mkOption {
-            type = types.str;
-            default = "oneshot";
-            description = "The systemd service run type";
-          };
-
-          os = mkOption {
-            type = types.str;
-            default = "linux";
-            description = "OS type of the container";
-          };
-
-          arch = mkOption {
-            type = types.str;
-            default = "x86_64";
-            description = "Computer architecture type of the container";
-          };
-        };
-      }));
-    };
-
-    stateDir = mkOption {
-      type = types.path;
-      default = "/var/railcar";
-      description = "Railcar persistent state directory";
-    };
-
-    package = mkOption {
-      type = types.package;
-      default = pkgs.railcar;
-      defaultText = literalExpression "pkgs.railcar";
-      description = "Railcar package to use";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services = flip mapAttrs' cfg.containers (name: containerConfig:
-      generateUnit name containerConfig
-    );
-  };
-}
-
diff --git a/nixos/modules/virtualisation/vmware-guest.nix b/nixos/modules/virtualisation/vmware-guest.nix
index 3caed746ca91e..d468a2008722f 100644
--- a/nixos/modules/virtualisation/vmware-guest.nix
+++ b/nixos/modules/virtualisation/vmware-guest.nix
@@ -64,7 +64,6 @@ in
     environment.etc.vmware-tools.source = "${open-vm-tools}/etc/vmware-tools/*";
 
     services.xserver = mkIf (!cfg.headless) {
-      videoDrivers = mkOverride 50 [ "vmware" ];
       modules = [ xf86inputvmmouse ];
 
       config = ''
diff --git a/nixos/modules/virtualisation/vmware-host.nix b/nixos/modules/virtualisation/vmware-host.nix
new file mode 100644
index 0000000000000..faa0d455c9d6b
--- /dev/null
+++ b/nixos/modules/virtualisation/vmware-host.nix
@@ -0,0 +1,166 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.virtualisation.vmware.host;
+  wrapperDir = "/run/vmware/bin"; # Perfectly fits as /usr/local/bin
+  parentWrapperDir = dirOf wrapperDir;
+  vmwareWrappers = # Needed as hardcoded paths workaround
+    let mkVmwareSymlink =
+      program:
+      ''
+        ln -s "${config.security.wrapperDir}/${program}" $wrapperDir/${program}
+      '';
+    in
+    [
+      (mkVmwareSymlink "pkexec")
+      (mkVmwareSymlink "mount")
+      (mkVmwareSymlink "umount")
+    ];
+in
+{
+  options = with lib; {
+    virtualisation.vmware.host = {
+      enable = mkEnableOption "VMware" // {
+        description = ''
+          This enables VMware host virtualisation for running VMs.
+
+          <important><para>
+          <literal>vmware-vmx</literal> will cause kcompactd0 due to
+          <literal>Transparent Hugepages</literal> feature in kernel.
+          Apply <literal>[ "transparent_hugepage=never" ]</literal> in
+          option <option>boot.kernelParams</option> to disable them.
+          </para></important>
+
+          <note><para>
+          If that didn't work disable <literal>TRANSPARENT_HUGEPAGE</literal>,
+          <literal>COMPACTION</literal> configs and recompile kernel.
+          </para></note>
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.vmware-workstation;
+        defaultText = literalExpression "pkgs.vmware-workstation";
+        description = "VMware host virtualisation package to use";
+      };
+      extraPackages = mkOption {
+        type = with types; listOf package;
+        default = with pkgs; [ ];
+        description = "Extra packages to be used with VMware host.";
+        example = "with pkgs; [ ntfs3g ]";
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Add extra config to /etc/vmware/config";
+        example = ''
+          # Allow unsupported device's OpenGL and Vulkan acceleration for guest vGPU
+          mks.gl.allowUnsupportedDrivers = "TRUE"
+          mks.vk.allowUnsupportedDevices = "TRUE"
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    boot.extraModulePackages = [ config.boot.kernelPackages.vmware ];
+    boot.extraModprobeConfig = "alias char-major-10-229 fuse";
+    boot.kernelModules = [ "vmw_pvscsi" "vmw_vmci" "vmmon" "vmnet" "fuse" ];
+
+    environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
+    services.printing.drivers = [ cfg.package ];
+
+    environment.etc."vmware/config".text = ''
+      ${builtins.readFile "${cfg.package}/etc/vmware/config"}
+      ${cfg.extraConfig}
+    '';
+
+    environment.etc."vmware/bootstrap".source = "${cfg.package}/etc/vmware/bootstrap";
+    environment.etc."vmware/icu".source = "${cfg.package}/etc/vmware/icu";
+    environment.etc."vmware-installer".source = "${cfg.package}/etc/vmware-installer";
+
+    # SUID wrappers
+
+    security.wrappers = {
+      vmware-vmx = {
+        setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${cfg.package}/lib/vmware/bin/.vmware-vmx-wrapped";
+      };
+    };
+
+    ###### wrappers activation script
+
+    system.activationScripts.vmwareWrappers =
+      lib.stringAfter [ "specialfs" "users" ]
+        ''
+          mkdir -p "${parentWrapperDir}"
+          chmod 755 "${parentWrapperDir}"
+          # We want to place the tmpdirs for the wrappers to the parent dir.
+          wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
+          chmod a+rx "$wrapperDir"
+          ${lib.concatStringsSep "\n" (vmwareWrappers)}
+          if [ -L ${wrapperDir} ]; then
+            # Atomically replace the symlink
+            # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
+            old=$(readlink -f ${wrapperDir})
+            if [ -e "${wrapperDir}-tmp" ]; then
+              rm --force --recursive "${wrapperDir}-tmp"
+            fi
+            ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
+            mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
+            rm --force --recursive "$old"
+          else
+            # For initial setup
+            ln --symbolic "$wrapperDir" "${wrapperDir}"
+          fi
+        '';
+
+    # Services
+
+    systemd.services."vmware-authdlauncher" = {
+      description = "VMware Authentification Daemon";
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = [ "${cfg.package}/bin/vmware-authdlauncher" ];
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services."vmware-networks-configuration" = {
+      description = "VMware Networks Configuration Generation";
+      unitConfig.ConditionPathExists = "!/etc/vmware/networking";
+      serviceConfig = {
+        UMask = "0077";
+        ExecStart = [
+          "${cfg.package}/bin/vmware-networks --postinstall vmware-player,0,1"
+        ];
+        Type = "oneshot";
+        RemainAfterExit = "yes";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services."vmware-networks" = {
+      description = "VMware Networks";
+      after = [ "vmware-networks-configuration.service" ];
+      requires = [ "vmware-networks-configuration.service" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecCondition = [ "${pkgs.kmod}/bin/modprobe vmnet" ];
+        ExecStart = [ "${cfg.package}/bin/vmware-networks --start" ];
+        ExecStop = [ "${cfg.package}/bin/vmware-networks --stop" ];
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services."vmware-usbarbitrator" = {
+      description = "VMware USB Arbitrator";
+      serviceConfig = {
+        ExecStart = [ "${cfg.package}/bin/vmware-usbarbitrator -f" ];
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/waydroid.nix b/nixos/modules/virtualisation/waydroid.nix
index 4fc798ff39f89..2c0b658948dd5 100644
--- a/nixos/modules/virtualisation/waydroid.nix
+++ b/nixos/modules/virtualisation/waydroid.nix
@@ -56,8 +56,6 @@ in
 
       wantedBy = [ "multi-user.target" ];
 
-      path = with pkgs; [ getent iptables iproute kmod nftables util-linux which ];
-
       unitConfig = {
         ConditionPathExists = "/var/lib/waydroid/lxc/waydroid";
       };
@@ -68,6 +66,10 @@ in
         ExecStopPost = "${pkgs.waydroid}/bin/waydroid session stop";
       };
     };
+
+    systemd.tmpfiles.rules = [
+      "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
+    ];
   };
 
 }
diff --git a/nixos/modules/virtualisation/xen-dom0.nix b/nixos/modules/virtualisation/xen-dom0.nix
index 975eed10cd267..a999efcb44e64 100644
--- a/nixos/modules/virtualisation/xen-dom0.nix
+++ b/nixos/modules/virtualisation/xen-dom0.nix
@@ -23,12 +23,12 @@ in
         default = false;
         type = types.bool;
         description =
-          ''
+          mdDoc ''
             Setting this option enables the Xen hypervisor, a
             virtualisation technology that allows multiple virtual
-            machines, known as <emphasis>domains</emphasis>, to run
+            machines, known as *domains*, to run
             concurrently on the physical machine.  NixOS runs as the
-            privileged <emphasis>Domain 0</emphasis>.  This option
+            privileged *Domain 0*.  This option
             requires a reboot to take effect.
           '';
       };
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index ee3f3d19174e7..7f81ca1c69b88 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -98,7 +98,6 @@ in rec {
         (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")
@@ -130,14 +129,17 @@ in rec {
         (onFullSupported "nixos.tests.networking.networkd.virtual")
         (onFullSupported "nixos.tests.networking.networkd.vlan")
         (onFullSupported "nixos.tests.systemd-networkd-ipv6-prefix-delegation")
-        (onFullSupported "nixos.tests.nfs3.simple")
+        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
+        #(onFullSupported "nixos.tests.nfs3.simple")
         (onFullSupported "nixos.tests.nfs4.simple")
+        (onSystems ["x86_64-linux"] "nixos.tests.oci-containers.podman")
         (onFullSupported "nixos.tests.openssh")
         (onFullSupported "nixos.tests.pantheon")
         (onFullSupported "nixos.tests.php.fpm")
         (onFullSupported "nixos.tests.php.httpd")
         (onFullSupported "nixos.tests.php.pcre")
         (onFullSupported "nixos.tests.plasma5")
+        (onSystems ["x86_64-linux"] "nixos.tests.podman")
         (onFullSupported "nixos.tests.predictable-interface-names.predictableNetworkd")
         (onFullSupported "nixos.tests.predictable-interface-names.predictable")
         (onFullSupported "nixos.tests.predictable-interface-names.unpredictableNetworkd")
diff --git a/nixos/release-small.nix b/nixos/release-small.nix
index 996db54c9a402..1d51b4e7f28f8 100644
--- a/nixos/release-small.nix
+++ b/nixos/release-small.nix
@@ -38,7 +38,8 @@ in rec {
         login
         misc
         nat
-        nfs3
+        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
+        #nfs3
         openssh
         php
         predictable-interface-names
@@ -107,7 +108,8 @@ in rec {
         "nixos.tests.nat.firewall-conntrack.x86_64-linux"
         "nixos.tests.nat.firewall.x86_64-linux"
         "nixos.tests.nat.standalone.x86_64-linux"
-        "nixos.tests.nfs3.simple.x86_64-linux"
+        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
+        #"nixos.tests.nfs3.simple.x86_64-linux"
         "nixos.tests.openssh.x86_64-linux"
         "nixos.tests.php.fpm.x86_64-linux"
         "nixos.tests.php.pcre.x86_64-linux"
diff --git a/nixos/release.nix b/nixos/release.nix
index 6b7564a9b9721..f70b02c4292b4 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -15,8 +15,9 @@ let
     (if stableBranch then "." else "pre") + "${toString nixpkgs.revCount}.${nixpkgs.shortRev}";
 
   # Run the tests for each platform.  You can run a test by doing
-  # e.g. ‘nix-build -A tests.login.x86_64-linux’, or equivalently,
-  # ‘nix-build tests/login.nix -A result’.
+  # e.g. ‘nix-build release.nix -A tests.login.x86_64-linux’,
+  # or equivalently, ‘nix-build tests/login.nix’.
+  # See also nixosTests in pkgs/top-level/all-packages.nix
   allTestsForSystem = system:
     import ./tests/all-tests.nix {
       inherit system;
@@ -24,7 +25,19 @@ let
       callTest = t: {
         ${system} = hydraJob t.test;
       };
+    } // {
+      # for typechecking of the scripts and evaluation of
+      # the nodes, without running VMs.
+      allDrivers =
+        import ./tests/all-tests.nix {
+        inherit system;
+        pkgs = import ./.. { inherit system; };
+        callTest = t: {
+          ${system} = hydraJob t.test.driver;
+        };
+      };
     };
+
   allTests =
     foldAttrs recursiveUpdate {} (map allTestsForSystem supportedSystems);
 
@@ -138,6 +151,13 @@ in rec {
   # Build the initial ramdisk so Hydra can keep track of its size over time.
   initialRamdisk = buildFromConfig ({ ... }: { }) (config: config.system.build.initialRamdisk);
 
+  kexec = forMatchingSystems supportedSystems (system: (import lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ./modules/installer/netboot/netboot-minimal.nix
+    ];
+  }).config.system.build.kexecTree);
+
   netboot = forMatchingSystems supportedSystems (system: makeNetboot {
     module = ./modules/installer/netboot/netboot-minimal.nix;
     inherit system;
@@ -150,13 +170,13 @@ in rec {
   });
 
   iso_plasma5 = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
-    module = ./modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix;
+    module = ./modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma5.nix;
     type = "plasma5";
     inherit system;
   });
 
   iso_gnome = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
-    module = ./modules/installer/cd-dvd/installation-cd-graphical-gnome.nix;
+    module = ./modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix;
     type = "gnome";
     inherit system;
   });
@@ -201,6 +221,29 @@ in rec {
 
   );
 
+  # KVM image for proxmox in VMA format
+  proxmoxImage = forMatchingSystems [ "x86_64-linux" ] (system:
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules = [
+        ./modules/virtualisation/proxmox-image.nix
+      ];
+    }).config.system.build.VMA)
+  );
+
+  # LXC tarball for proxmox
+  proxmoxLXC = forMatchingSystems [ "x86_64-linux" ] (system:
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules = [
+        ./modules/virtualisation/proxmox-lxc.nix
+      ];
+    }).config.system.build.tarball)
+  );
 
   # A disk image that can be imported to Amazon EC2 and registered as an AMI
   amazonImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
@@ -298,39 +341,12 @@ in rec {
     "mkdir $out; ln -s $toplevel $out/dummy");
 
 
-  # Provide a tarball that can be unpacked into an SD card, and easily
-  # boot that system from uboot (like for the sheevaplug).
-  # The pc variant helps preparing the expression for the system tarball
-  # in a machine faster than the sheevpalug
-  /*
-  system_tarball_pc = forAllSystems (system: makeSystemTarball {
-    module = ./modules/installer/cd-dvd/system-tarball-pc.nix;
-    inherit system;
-  });
-  */
-
   # Provide container tarball for lxc, libvirt-lxc, docker-lxc, ...
   containerTarball = forAllSystems (system: makeSystemTarball {
     module = ./modules/virtualisation/lxc-container.nix;
     inherit system;
   });
 
-  /*
-  system_tarball_fuloong2f =
-    assert builtins.currentSystem == "mips64-linux";
-    makeSystemTarball {
-      module = ./modules/installer/cd-dvd/system-tarball-fuloong2f.nix;
-      system = "mips64-linux";
-    };
-
-  system_tarball_sheevaplug =
-    assert builtins.currentSystem == "armv5tel-linux";
-    makeSystemTarball {
-      module = ./modules/installer/cd-dvd/system-tarball-sheevaplug.nix;
-      system = "armv5tel-linux";
-    };
-  */
-
   tests = allTests;
 
   /* Build a bunch of typical closures so that Hydra can keep track of
diff --git a/nixos/tests/3proxy.nix b/nixos/tests/3proxy.nix
index dfc4b35a772dc..8127438fabd97 100644
--- a/nixos/tests/3proxy.nix
+++ b/nixos/tests/3proxy.nix
@@ -139,7 +139,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     peer0.wait_for_unit("network-online.target")
 
     peer1.wait_for_unit("3proxy.service")
-    peer1.wait_for_open_port("9999")
+    peer1.wait_for_open_port(9999)
 
     # test none auth
     peer0.succeed(
@@ -153,7 +153,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     )
 
     peer2.wait_for_unit("3proxy.service")
-    peer2.wait_for_open_port("9999")
+    peer2.wait_for_open_port(9999)
 
     # test iponly auth
     peer0.succeed(
@@ -167,7 +167,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     )
 
     peer3.wait_for_unit("3proxy.service")
-    peer3.wait_for_open_port("9999")
+    peer3.wait_for_open_port(9999)
 
     # test strong auth
     peer0.succeed(
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 2dd06a50f40ba..c07f99c5db3a2 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -578,7 +578,7 @@ in {
               webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
               wait_for_server()
               check_connection(client, test_domain)
-              rc, _ = client.execute(
+              rc, _s = client.execute(
                   f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443"
                   " </dev/null 2>/dev/null | openssl x509 -noout -text"
                   f" | grep DNS: | grep {test_alias}"
diff --git a/nixos/tests/aesmd.nix b/nixos/tests/aesmd.nix
index 59c04fe7e96a3..9f07426be8d8e 100644
--- a/nixos/tests/aesmd.nix
+++ b/nixos/tests/aesmd.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ veehaitch ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     services.aesmd = {
       enable = true;
       settings = {
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
index ec61af2afe754..6f51300111acf 100644
--- a/nixos/tests/agda.nix
+++ b/nixos/tests/agda.nix
@@ -15,7 +15,7 @@ in
     maintainers = [ alexarice turion ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [
       (pkgs.agda.withPackages {
         pkgs = p: [ p.standard-library ];
diff --git a/nixos/tests/airsonic.nix b/nixos/tests/airsonic.nix
index d8df092c2ecfa..69f979726bce4 100644
--- a/nixos/tests/airsonic.nix
+++ b/nixos/tests/airsonic.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ sumnerevans ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     {
       services.airsonic = {
@@ -15,7 +15,8 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   testScript = ''
     def airsonic_is_up(_) -> bool:
-        return machine.succeed("curl --fail http://localhost:4040/login")
+        status, _ = machine.execute("curl --fail http://localhost:4040/login")
+        return status == 0
 
 
     machine.start()
diff --git a/nixos/tests/all-terminfo.nix b/nixos/tests/all-terminfo.nix
new file mode 100644
index 0000000000000..dd47c66ee1c1e
--- /dev/null
+++ b/nixos/tests/all-terminfo.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ... }: rec {
+  name = "all-terminfo";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ jkarlson ];
+  };
+
+  nodes.machine = { pkgs, config, lib, ... }:
+    let
+      infoFilter = name: drv:
+        let
+          o = builtins.tryEval drv;
+        in
+        o.success && lib.isDerivation o.value && o.value ? outputs && builtins.elem "terminfo" o.value.outputs;
+      terminfos = lib.filterAttrs infoFilter pkgs;
+      excludedTerminfos = lib.filterAttrs (_: drv: !(builtins.elem drv.terminfo config.environment.systemPackages)) terminfos;
+      includedOuts = lib.filterAttrs (_: drv: builtins.elem drv.out config.environment.systemPackages) terminfos;
+    in
+    {
+      environment = {
+        enableAllTerminfo = true;
+        etc."terminfo-missing".text = builtins.concatStringsSep "\n" (builtins.attrNames excludedTerminfos);
+        etc."terminfo-extra-outs".text = builtins.concatStringsSep "\n" (builtins.attrNames includedOuts);
+      };
+    };
+
+  testScript =
+    ''
+      machine.fail("grep . /etc/terminfo-missing >&2")
+      machine.fail("grep . /etc/terminfo-extra-outs >&2")
+    '';
+})
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 06ebf53dd80bc..b0dd7ca0766fe 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -26,14 +26,16 @@ let
     featureFlags.minimalModules = {};
   };
   evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
-in
-{
+
+in {
   _3proxy = handleTest ./3proxy.nix {};
   acme = handleTest ./acme.nix {};
   adguardhome = handleTest ./adguardhome.nix {};
   aesmd = handleTest ./aesmd.nix {};
+  agate = handleTest ./web-servers/agate.nix {};
   agda = handleTest ./agda.nix {};
   airsonic = handleTest ./airsonic.nix {};
+  allTerminfo = handleTest ./all-terminfo.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
   apfs = handleTest ./apfs.nix {};
   apparmor = handleTest ./apparmor.nix {};
@@ -51,6 +53,7 @@ in
   bitcoind = handleTest ./bitcoind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
   blockbook-frontend = handleTest ./blockbook-frontend.nix {};
+  blocky = handleTest ./blocky.nix {};
   boot = handleTestOn ["x86_64-linux" "aarch64-linux"] ./boot.nix {};
   boot-stage1 = handleTest ./boot-stage1.nix {};
   borgbackup = handleTest ./borgbackup.nix {};
@@ -59,6 +62,7 @@ in
   breitbandmessung = handleTest ./breitbandmessung.nix {};
   brscan5 = handleTest ./brscan5.nix {};
   btrbk = handleTest ./btrbk.nix {};
+  btrbk-no-timer = handleTest ./btrbk-no-timer.nix {};
   buildbot = handleTest ./buildbot.nix {};
   buildkite-agents = handleTest ./buildkite-agents.nix {};
   caddy = handleTest ./caddy.nix {};
@@ -106,9 +110,8 @@ in
   cri-o = handleTestOn ["x86_64-linux"] ./cri-o.nix {};
   custom-ca = handleTest ./custom-ca.nix {};
   croc = handleTest ./croc.nix {};
-  cryptpad = handleTest ./cryptpad.nix {};
   deluge = handleTest ./deluge.nix {};
-  dendrite = handleTest ./dendrite.nix {};
+  dendrite = handleTest ./matrix/dendrite.nix {};
   dex-oidc = handleTest ./dex-oidc.nix {};
   dhparams = handleTest ./dhparams.nix {};
   disable-installer-tools = handleTest ./disable-installer-tools.nix {};
@@ -119,7 +122,6 @@ in
   doas = handleTest ./doas.nix {};
   docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
   docker-rootless = handleTestOn ["x86_64-linux"] ./docker-rootless.nix {};
-  docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
   docker-registry = handleTest ./docker-registry.nix {};
   docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
   docker-tools-cross = handleTestOn ["x86_64-linux" "aarch64-linux"] ./docker-tools-cross.nix {};
@@ -130,6 +132,7 @@ in
   domination = handleTest ./domination.nix {};
   dovecot = handleTest ./dovecot.nix {};
   drbd = handleTest ./drbd.nix {};
+  earlyoom = handleTestOn ["x86_64-linux"] ./earlyoom.nix {};
   ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
   ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
   ecryptfs = handleTest ./ecryptfs.nix {};
@@ -139,6 +142,7 @@ in
   engelsystem = handleTest ./engelsystem.nix {};
   enlightenment = handleTest ./enlightenment.nix {};
   env = handleTest ./env.nix {};
+  envoy = handleTest ./envoy.nix {};
   ergo = handleTest ./ergo.nix {};
   ergochat = handleTest ./ergochat.nix {};
   etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
@@ -146,6 +150,7 @@ in
   etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
   etebase-server = handleTest ./etebase-server.nix {};
   etesync-dav = handleTest ./etesync-dav.nix {};
+  extra-python-packages = handleTest ./extra-python-packages.nix {};
   fancontrol = handleTest ./fancontrol.nix {};
   fcitx = handleTest ./fcitx {};
   fenics = handleTest ./fenics.nix {};
@@ -153,6 +158,7 @@ in
   firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; };
   firefox-esr    = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
   firefox-esr-91 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-91; };
+  firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
   firejail = handleTest ./firejail.nix {};
   firewall = handleTest ./firewall.nix {};
   fish = handleTest ./fish.nix {};
@@ -164,6 +170,7 @@ in
   frr = handleTest ./frr.nix {};
   fsck = handleTest ./fsck.nix {};
   ft2-clone = handleTest ./ft2-clone.nix {};
+  mimir = handleTest ./mimir.nix {};
   gerrit = handleTest ./gerrit.nix {};
   geth = handleTest ./geth.nix {};
   ghostunnel = handleTest ./ghostunnel.nix {};
@@ -182,17 +189,23 @@ in
   google-oslogin = handleTest ./google-oslogin {};
   gotify-server = handleTest ./gotify-server.nix {};
   grafana = handleTest ./grafana.nix {};
+  grafana-agent = handleTest ./grafana-agent.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
   grocy = handleTest ./grocy.nix {};
   grub = handleTest ./grub.nix {};
   gvisor = handleTest ./gvisor.nix {};
-  hadoop.all = handleTestOn [ "x86_64-linux" ] ./hadoop/hadoop.nix {};
-  hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
-  hadoop.yarn = handleTestOn [ "x86_64-linux" ] ./hadoop/yarn.nix {};
+  hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; };
+  hadoop_3_2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_2; };
+  hadoop2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop2; };
   haka = handleTest ./haka.nix {};
+  haste-server = handleTest ./haste-server.nix {};
   haproxy = handleTest ./haproxy.nix {};
   hardened = handleTest ./hardened.nix {};
+  healthchecks = handleTest ./web-apps/healthchecks.nix {};
+  hbase1 = handleTest ./hbase.nix { package=pkgs.hbase1; };
+  hbase2 = handleTest ./hbase.nix { package=pkgs.hbase2; };
+  hbase3 = handleTest ./hbase.nix { package=pkgs.hbase3; };
   hedgedoc = handleTest ./hedgedoc.nix {};
   herbstluftwm = handleTest ./herbstluftwm.nix {};
   installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
@@ -203,6 +216,7 @@ in
   # hibernation. This test happens to work on x86_64-linux but
   # not on other platforms.
   hibernate = handleTestOn ["x86_64-linux"] ./hibernate.nix {};
+  hibernate-systemd-stage-1 = handleTestOn ["x86_64-linux"] ./hibernate.nix { systemdStage1 = true; };
   hitch = handleTest ./hitch {};
   hledger-web = handleTest ./hledger-web.nix {};
   hocker-fetchdocker = handleTest ./hocker-fetchdocker {};
@@ -222,8 +236,10 @@ in
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
   initrdNetwork = handleTest ./initrd-network.nix {};
   initrd-secrets = handleTest ./initrd-secrets.nix {};
+  input-remapper = handleTest ./input-remapper.nix {};
   inspircd = handleTest ./inspircd.nix {};
   installer = handleTest ./installer.nix {};
+  installer-systemd-stage-1 = handleTest ./installer-systemd-stage-1.nix {};
   invoiceplane = handleTest ./invoiceplane.nix {};
   iodine = handleTest ./iodine.nix {};
   ipfs = handleTest ./ipfs.nix {};
@@ -239,8 +255,8 @@ in
   jirafeau = handleTest ./jirafeau.nix {};
   jitsi-meet = handleTest ./jitsi-meet.nix {};
   k3s-single-node = handleTest ./k3s-single-node.nix {};
-  k3s-single-node-docker = handleTest ./k3s-single-node-docker.nix {};
   kafka = handleTest ./kafka.nix {};
+  kanidm = handleTest ./kanidm.nix {};
   kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
   kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {};
   kea = handleTest ./kea.nix {};
@@ -262,30 +278,36 @@ in
   libreddit = handleTest ./libreddit.nix {};
   libresprite = handleTest ./libresprite.nix {};
   libreswan = handleTest ./libreswan.nix {};
+  librewolf = handleTest ./firefox.nix { firefoxPackage = pkgs.librewolf; };
+  libuiohook = handleTest ./libuiohook.nix {};
   lidarr = handleTest ./lidarr.nix {};
   lightdm = handleTest ./lightdm.nix {};
+  lighttpd = handleTest ./lighttpd.nix {};
   limesurvey = handleTest ./limesurvey.nix {};
   litestream = handleTest ./litestream.nix {};
   locate = handleTest ./locate.nix {};
   login = handleTest ./login.nix {};
+  logrotate = handleTest ./logrotate.nix {};
   loki = handleTest ./loki.nix {};
+  lvm2 = handleTest ./lvm2 {};
   lxd = handleTest ./lxd.nix {};
-  lxd-image = handleTest ./lxd-image.nix {};
   lxd-nftables = handleTest ./lxd-nftables.nix {};
   lxd-image-server = handleTest ./lxd-image-server.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
   maddy = handleTest ./maddy.nix {};
+  maestral = handleTest ./maestral.nix {};
   magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
   magnetico = handleTest ./magnetico.nix {};
   mailcatcher = handleTest ./mailcatcher.nix {};
   mailhog = handleTest ./mailhog.nix {};
   man = handleTest ./man.nix {};
   mariadb-galera = handleTest ./mysql/mariadb-galera.nix {};
+  mastodon = handleTestOn ["x86_64-linux" "i686-linux" "aarch64-linux"] ./web-apps/mastodon.nix {};
   matomo = handleTest ./matomo.nix {};
-  matrix-appservice-irc = handleTest ./matrix-appservice-irc.nix {};
-  matrix-conduit = handleTest ./matrix-conduit.nix {};
-  matrix-synapse = handleTest ./matrix-synapse.nix {};
+  matrix-appservice-irc = handleTest ./matrix/appservice-irc.nix {};
+  matrix-conduit = handleTest ./matrix/conduit.nix {};
+  matrix-synapse = handleTest ./matrix/synapse.nix {};
   mattermost = handleTest ./mattermost.nix {};
   mediatomb = handleTest ./mediatomb.nix {};
   mediawiki = handleTest ./mediawiki.nix {};
@@ -303,11 +325,13 @@ in
   molly-brown = handleTest ./molly-brown.nix {};
   mongodb = handleTest ./mongodb.nix {};
   moodle = handleTest ./moodle.nix {};
+  moonraker = handleTest ./moonraker.nix {};
   morty = handleTest ./morty.nix {};
   mosquitto = handleTest ./mosquitto.nix {};
   moosefs = handleTest ./moosefs.nix {};
   mpd = handleTest ./mpd.nix {};
   mpv = handleTest ./mpv.nix {};
+  mtp = handleTest ./mtp.nix {};
   mumble = handleTest ./mumble.nix {};
   musescore = handleTest ./musescore.nix {};
   munin = handleTest ./munin.nix {};
@@ -319,13 +343,13 @@ in
   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; };
   nats = handleTest ./nats.nix {};
   navidrome = handleTest ./navidrome.nix {};
+  nbd = handleTest ./nbd.nix {};
   ncdns = handleTest ./ncdns.nix {};
   ndppd = handleTest ./ndppd.nix {};
   nebula = handleTest ./nebula.nix {};
@@ -334,6 +358,7 @@ in
   networking.networkd = handleTest ./networking.nix { networkd = true; };
   networking.scripted = handleTest ./networking.nix { networkd = false; };
   specialisation = handleTest ./specialisation.nix {};
+  netbox = handleTest ./web-apps/netbox.nix {};
   # TODO: put in networking.nix after the test becomes more complete
   networkingProxy = handleTest ./networking-proxy.nix {};
   nextcloud = handleTest ./nextcloud {};
@@ -345,11 +370,15 @@ in
   nginx = handleTest ./nginx.nix {};
   nginx-auth = handleTest ./nginx-auth.nix {};
   nginx-etag = handleTest ./nginx-etag.nix {};
+  nginx-http3 = handleTest ./nginx-http3.nix {};
+  nginx-modsecurity = handleTest ./nginx-modsecurity.nix {};
   nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
   nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
   nginx-sso = handleTest ./nginx-sso.nix {};
   nginx-variants = handleTest ./nginx-variants.nix {};
+  nifi = handleTestOn ["x86_64-linux"] ./web-apps/nifi.nix {};
   nitter = handleTest ./nitter.nix {};
+  nix-ld = handleTest ./nix-ld.nix {};
   nix-serve = handleTest ./nix-serve.nix {};
   nix-serve-ssh = handleTest ./nix-serve-ssh.nix {};
   nixops = handleTest ./nixops/default.nix {};
@@ -357,6 +386,7 @@ in
   nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; };
   node-red = handleTest ./node-red.nix {};
   nomad = handleTest ./nomad.nix {};
+  non-default-filesystems = handleTest ./non-default-filesystems.nix {};
   noto-fonts = handleTest ./noto-fonts.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nsd = handleTest ./nsd.nix {};
@@ -379,23 +409,28 @@ in
   os-prober = handleTestOn ["x86_64-linux"] ./os-prober.nix {};
   osrm-backend = handleTest ./osrm-backend.nix {};
   overlayfs = handleTest ./overlayfs.nix {};
+  pacemaker = handleTest ./pacemaker.nix {};
   packagekit = handleTest ./packagekit.nix {};
   pam-file-contents = handleTest ./pam/pam-file-contents.nix {};
   pam-oath-login = handleTest ./pam/pam-oath-login.nix {};
   pam-u2f = handleTest ./pam/pam-u2f.nix {};
+  pam-ussh = handleTest ./pam/pam-ussh.nix {};
+  pass-secret-service = handleTest ./pass-secret-service.nix {};
   pantalaimon = handleTest ./matrix/pantalaimon.nix {};
   pantheon = handleTest ./pantheon.nix {};
-  paperless-ng = handleTest ./paperless-ng.nix {};
+  paperless = handleTest ./paperless.nix {};
   parsedmarc = handleTest ./parsedmarc {};
   pdns-recursor = handleTest ./pdns-recursor.nix {};
   peerflix = handleTest ./peerflix.nix {};
   peertube = handleTestOn ["x86_64-linux"] ./web-apps/peertube.nix {};
+  pgadmin4 = handleTest ./pgadmin4.nix {};
+  pgadmin4-standalone = handleTest ./pgadmin4-standalone.nix {};
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   php = handleTest ./php {};
-  php74 = handleTest ./php { php = pkgs.php74; };
   php80 = handleTest ./php { php = pkgs.php80; };
   php81 = handleTest ./php { php = pkgs.php81; };
+  phylactery = handleTest ./web-apps/phylactery.nix {};
   pict-rs = handleTest ./pict-rs.nix {};
   pinnwand = handleTest ./pinnwand.nix {};
   plasma5 = handleTest ./plasma5.nix {};
@@ -408,6 +443,7 @@ in
   podman = handleTestOn ["x86_64-linux"] ./podman/default.nix {};
   podman-dnsname = handleTestOn ["x86_64-linux"] ./podman/dnsname.nix {};
   podman-tls-ghostunnel = handleTestOn ["x86_64-linux"] ./podman/tls-ghostunnel.nix {};
+  polaris = handleTest ./polaris.nix {};
   pomerium = handleTestOn ["x86_64-linux"] ./pomerium.nix {};
   postfix = handleTest ./postfix.nix {};
   postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
@@ -426,9 +462,12 @@ in
   prometheus = handleTest ./prometheus.nix {};
   prometheus-exporters = handleTest ./prometheus-exporters.nix {};
   prosody = handleTest ./xmpp/prosody.nix {};
+  prosody-mysql = handleTest ./xmpp/prosody-mysql.nix {};
   proxy = handleTest ./proxy.nix {};
   prowlarr = handleTest ./prowlarr.nix {};
   pt2-clone = handleTest ./pt2-clone.nix {};
+  pykms = handleTest ./pykms.nix {};
+  public-inbox = handleTest ./public-inbox.nix {};
   pulseaudio = discoverTests (import ./pulseaudio.nix);
   qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
   quorum = handleTest ./quorum.nix {};
@@ -442,7 +481,6 @@ in
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
   restic = handleTest ./restic.nix {};
   retroarch = handleTest ./retroarch.nix {};
-  riak = handleTest ./riak.nix {};
   robustirc-bridge = handleTest ./robustirc-bridge.nix {};
   roundcube = handleTest ./roundcube.nix {};
   rspamd = handleTest ./rspamd.nix {};
@@ -455,10 +493,12 @@ in
   samba = handleTest ./samba.nix {};
   samba-wsdd = handleTest ./samba-wsdd.nix {};
   sanoid = handleTest ./sanoid.nix {};
+  schleuder = handleTest ./schleuder.nix {};
   sddm = handleTest ./sddm.nix {};
   seafile = handleTest ./seafile.nix {};
   searx = handleTest ./searx.nix {};
   service-runner = handleTest ./service-runner.nix {};
+  sfxr-qt = handleTest ./sfxr-qt.nix {};
   shadow = handleTest ./shadow.nix {};
   shadowsocks = handleTest ./shadowsocks {};
   shattered-pixel-dungeon = handleTest ./shattered-pixel-dungeon.nix {};
@@ -476,7 +516,7 @@ in
   sonarr = handleTest ./sonarr.nix {};
   sourcehut = handleTest ./sourcehut.nix {};
   spacecookie = handleTest ./spacecookie.nix {};
-  spark = handleTestOn ["x86_64-linux"] ./spark {};
+  spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark {};
   sslh = handleTest ./sslh.nix {};
   sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {};
   sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {};
@@ -485,6 +525,7 @@ in
   strongswan-swanctl = handleTest ./strongswan-swanctl.nix {};
   stunnel = handleTest ./stunnel.nix {};
   sudo = handleTest ./sudo.nix {};
+  swap-partition = handleTest ./swap-partition.nix {};
   sway = handleTest ./sway.nix {};
   switchTest = handleTest ./switch-test.nix {};
   sympa = handleTest ./sympa.nix {};
@@ -497,26 +538,37 @@ in
   systemd-boot = handleTest ./systemd-boot.nix {};
   systemd-confinement = handleTest ./systemd-confinement.nix {};
   systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
+  systemd-escaping = handleTest ./systemd-escaping.nix {};
+  systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {};
+  systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {};
+  systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {};
+  systemd-initrd-shutdown = handleTest ./systemd-shutdown.nix { systemdStage1 = true; };
+  systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
+  systemd-initrd-swraid = handleTest ./systemd-initrd-swraid.nix {};
   systemd-journal = handleTest ./systemd-journal.nix {};
+  systemd-machinectl = handleTest ./systemd-machinectl.nix {};
   systemd-networkd = handleTest ./systemd-networkd.nix {};
   systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {};
   systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {};
   systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {};
   systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
+  systemd-shutdown = handleTest ./systemd-shutdown.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
-  systemd-unit-path = handleTest ./systemd-unit-path.nix {};
+  systemd-misc = handleTest ./systemd-misc.nix {};
   taskserver = handleTest ./taskserver.nix {};
   teeworlds = handleTest ./teeworlds.nix {};
   telegraf = handleTest ./telegraf.nix {};
   teleport = handleTest ./teleport.nix {};
   thelounge = handleTest ./thelounge.nix {};
+  terminal-emulators = handleTest ./terminal-emulators.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
   tigervnc = handleTest ./tigervnc.nix {};
   timezone = handleTest ./timezone.nix {};
   tinc = handleTest ./tinc {};
   tinydns = handleTest ./tinydns.nix {};
   tinywl = handleTest ./tinywl.nix {};
+  tomcat = handleTest ./tomcat.nix {};
   tor = handleTest ./tor.nix {};
   # traefik test relies on docker-containers
   traefik = handleTestOn ["x86_64-linux"] ./traefik.nix {};
@@ -536,11 +588,14 @@ in
   unifi = handleTest ./unifi.nix {};
   unit-php = handleTest ./web-servers/unit-php.nix {};
   upnp = handleTest ./upnp.nix {};
+  uptermd = handleTest ./uptermd.nix {};
   usbguard = handleTest ./usbguard.nix {};
   user-activation-scripts = handleTest ./user-activation-scripts.nix {};
+  user-home-mode = handleTest ./user-home-mode.nix {};
   uwsgi = handleTest ./uwsgi.nix {};
   v2ray = handleTest ./v2ray.nix {};
   vault = handleTest ./vault.nix {};
+  vault-dev = handleTest ./vault-dev.nix {};
   vault-postgresql = handleTest ./vault-postgresql.nix {};
   vaultwarden = handleTest ./vaultwarden.nix {};
   vector = handleTest ./vector.nix {};
@@ -549,6 +604,7 @@ in
   vikunja = handleTest ./vikunja.nix {};
   virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
   vscodium = discoverTests (import ./vscodium.nix);
+  vsftpd = handleTest ./vsftpd.nix {};
   wasabibackend = handleTest ./wasabibackend.nix {};
   wiki-js = handleTest ./wiki-js.nix {};
   wine = handleTest ./wine.nix {};
@@ -561,15 +617,19 @@ in
   xautolock = handleTest ./xautolock.nix {};
   xfce = handleTest ./xfce.nix {};
   xmonad = handleTest ./xmonad.nix {};
+  xmonad-xdg-autostart = handleTest ./xmonad-xdg-autostart.nix {};
   xrdp = handleTest ./xrdp.nix {};
   xss-lock = handleTest ./xss-lock.nix {};
   xterm = handleTest ./xterm.nix {};
   xxh = handleTest ./xxh.nix {};
   yabar = handleTest ./yabar.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
+  zammad = handleTest ./zammad.nix {};
+  zeronet-conservancy = handleTest ./zeronet-conservancy.nix {};
   zfs = handleTest ./zfs.nix {};
   zigbee2mqtt = handleTest ./zigbee2mqtt.nix {};
   zoneminder = handleTest ./zoneminder.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
+  zrepl = handleTest ./zrepl.nix {};
   zsh-history = handleTest ./zsh-history.nix {};
 }
diff --git a/nixos/tests/amazon-init-shell.nix b/nixos/tests/amazon-init-shell.nix
index f9268b2f3a009..3c040841b6d29 100644
--- a/nixos/tests/amazon-init-shell.nix
+++ b/nixos/tests/amazon-init-shell.nix
@@ -18,7 +18,7 @@ makeTest {
   meta = with maintainers; {
     maintainers = [ urbas ];
   };
-  machine = { ... }:
+  nodes.machine = { ... }:
   {
     imports = [ ../modules/profiles/headless.nix ../modules/virtualisation/amazon-init.nix ];
     services.openssh.enable = true;
diff --git a/nixos/tests/apfs.nix b/nixos/tests/apfs.nix
index a82886cbe7317..a8841fe93046e 100644
--- a/nixos/tests/apfs.nix
+++ b/nixos/tests/apfs.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "apfs";
   meta.maintainers = with pkgs.lib.maintainers; [ Luflosi ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.emptyDiskImages = [ 1024 ];
 
     boot.supportedFilesystems = [ "apfs" ];
diff --git a/nixos/tests/apparmor.nix b/nixos/tests/apparmor.nix
index c6daa8e67de3f..f85bff0295e71 100644
--- a/nixos/tests/apparmor.nix
+++ b/nixos/tests/apparmor.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     maintainers = [ julm ];
   };
 
-  machine =
+  nodes.machine =
     { lib, pkgs, config, ... }:
     with lib;
     {
diff --git a/nixos/tests/atd.nix b/nixos/tests/atd.nix
index ad4d60067cf1e..4342e9d7dc181 100644
--- a/nixos/tests/atd.nix
+++ b/nixos/tests/atd.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     maintainers = [ bjornfor ];
   };
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.atd.enable = true;
       users.users.alice = { isNormalUser = true; };
diff --git a/nixos/tests/atop.nix b/nixos/tests/atop.nix
index f7a90346f3d74..ec10369a24fd6 100644
--- a/nixos/tests/atop.nix
+++ b/nixos/tests/atop.nix
@@ -107,7 +107,7 @@ in
 {
   justThePackage = makeTest {
     name = "atop-justThePackage";
-    machine = {
+    nodes.machine = {
       environment.systemPackages = [ pkgs.atop ];
     };
     testScript = with assertions; builtins.concatStringsSep "\n" [
@@ -123,7 +123,7 @@ in
   };
   defaults = makeTest {
     name = "atop-defaults";
-    machine = {
+    nodes.machine = {
       programs.atop = {
         enable = true;
       };
@@ -141,7 +141,7 @@ in
   };
   minimal = makeTest {
     name = "atop-minimal";
-    machine = {
+    nodes.machine = {
       programs.atop = {
         enable = true;
         atopService.enable = false;
@@ -162,7 +162,7 @@ in
   };
   netatop = makeTest {
     name = "atop-netatop";
-    machine = {
+    nodes.machine = {
       programs.atop = {
         enable = true;
         netatop.enable = true;
@@ -181,11 +181,7 @@ in
   };
   atopgpu = makeTest {
     name = "atop-atopgpu";
-    machine = {
-      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [
-        "cudatoolkit"
-      ];
-
+    nodes.machine = {
       programs.atop = {
         enable = true;
         atopgpu.enable = true;
@@ -204,11 +200,7 @@ in
   };
   everything = makeTest {
     name = "atop-everthing";
-    machine = {
-      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [
-        "cudatoolkit"
-      ];
-
+    nodes.machine = {
       programs.atop = {
         enable = true;
         settings = {
diff --git a/nixos/tests/avahi.nix b/nixos/tests/avahi.nix
index ebb46838325f3..c53a95903291c 100644
--- a/nixos/tests/avahi.nix
+++ b/nixos/tests/avahi.nix
@@ -59,7 +59,7 @@ import ./make-test-python.nix {
     two.succeed("test `wc -l < out` -gt 0")
 
     # More DNS-SD.
-    one.execute('avahi-publish -s "This is a test" _test._tcp 123 one=1 &')
+    one.execute('avahi-publish -s "This is a test" _test._tcp 123 one=1 >&2 &')
     one.sleep(5)
     two.succeed("avahi-browse -r -t _test._tcp | tee out >&2")
     two.succeed("test `wc -l < out` -gt 0")
diff --git a/nixos/tests/bazarr.nix b/nixos/tests/bazarr.nix
index c3337611aa29a..efcd9de010804 100644
--- a/nixos/tests/bazarr.nix
+++ b/nixos/tests/bazarr.nix
@@ -20,7 +20,7 @@ in
 
   testScript = ''
     machine.wait_for_unit("bazarr.service")
-    machine.wait_for_open_port("${toString port}")
+    machine.wait_for_open_port(port)
     machine.succeed("curl --fail http://localhost:${toString port}/")
   '';
 })
diff --git a/nixos/tests/bcachefs.nix b/nixos/tests/bcachefs.nix
index 211195586ed9e..68ac49d0a6a6a 100644
--- a/nixos/tests/bcachefs.nix
+++ b/nixos/tests/bcachefs.nix
@@ -1,12 +1,12 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "bcachefs";
-  meta.maintainers = with pkgs.lib.maintainers; [ chiiruno ];
+  meta.maintainers = with pkgs.lib.maintainers; [ Madouura ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.emptyDiskImages = [ 4096 ];
     networking.hostId = "deadbeef";
     boot.supportedFilesystems = [ "bcachefs" ];
-    environment.systemPackages = with pkgs; [ parted ];
+    environment.systemPackages = with pkgs; [ parted keyutils ];
   };
 
   testScript = ''
@@ -20,10 +20,9 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         "parted --script /dev/vdb mklabel msdos",
         "parted --script /dev/vdb -- mkpart primary 1024M 50% mkpart primary 50% -1s",
         "udevadm settle",
-        # Due to #32279, we cannot use encryption for this test yet
-        # "echo password | bcachefs format --encrypted --metadata_replicas 2 --label vtest /dev/vdb1 /dev/vdb2",
-        # "echo password | bcachefs unlock /dev/vdb1",
-        "bcachefs format --metadata_replicas 2 --label vtest /dev/vdb1 /dev/vdb2",
+        "keyctl link @u @s",
+        "echo password | bcachefs format --encrypted --metadata_replicas 2 --label vtest /dev/vdb1 /dev/vdb2",
+        "echo password | bcachefs unlock /dev/vdb1",
         "mount -t bcachefs /dev/vdb1:/dev/vdb2 /tmp/mnt",
         "udevadm settle",
         "bcachefs fs usage /tmp/mnt",
diff --git a/nixos/tests/beanstalkd.nix b/nixos/tests/beanstalkd.nix
index 4f4a454fb47f9..518f018408ad8 100644
--- a/nixos/tests/beanstalkd.nix
+++ b/nixos/tests/beanstalkd.nix
@@ -28,7 +28,7 @@ in
   name = "beanstalkd";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.beanstalkd.enable = true;
     };
diff --git a/nixos/tests/bees.nix b/nixos/tests/bees.nix
index 58a9c29513567..3ab9f38ada8fe 100644
--- a/nixos/tests/bees.nix
+++ b/nixos/tests/bees.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
 {
   name = "bees";
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     boot.initrd.postDeviceCommands = ''
       ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux1 /dev/vdb
       ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux2 /dev/vdc
diff --git a/nixos/tests/bind.nix b/nixos/tests/bind.nix
index 7234f56a1c3a8..15accbd49db43 100644
--- a/nixos/tests/bind.nix
+++ b/nixos/tests/bind.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "bind";
 
-  machine = { pkgs, lib, ... }: {
+  nodes.machine = { pkgs, lib, ... }: {
     services.bind.enable = true;
     services.bind.extraOptions = "empty-zones-enable no;";
     services.bind.zones = lib.singleton {
diff --git a/nixos/tests/bird.nix b/nixos/tests/bird.nix
index 50d397be14ee0..822a7caea9ba3 100644
--- a/nixos/tests/bird.nix
+++ b/nixos/tests/bird.nix
@@ -9,7 +9,7 @@ let
   inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
   inherit (pkgs.lib) optionalString;
 
-  hostShared = hostId: { pkgs, ... }: {
+  makeBird2Host = hostId: { pkgs, ... }: {
     virtualisation.vlans = [ 1 ];
 
     environment.systemPackages = with pkgs; [ jq ];
@@ -24,105 +24,6 @@ let
       name = "eth1";
       networkConfig.Address = "10.0.0.${hostId}/24";
     };
-  };
-
-  birdTest = v4:
-    let variant = "bird${optionalString (!v4) "6"}"; in
-    makeTest {
-      name = variant;
-
-      nodes.host1 = makeBirdHost variant "1";
-      nodes.host2 = makeBirdHost variant "2";
-
-      testScript = makeTestScript variant v4 (!v4);
-    };
-
-  bird2Test = makeTest {
-    name = "bird2";
-
-    nodes.host1 = makeBird2Host "1";
-    nodes.host2 = makeBird2Host "2";
-
-    testScript = makeTestScript "bird2" true true;
-  };
-
-  makeTestScript = variant: v4: v6: ''
-    start_all()
-
-    host1.wait_for_unit("${variant}.service")
-    host2.wait_for_unit("${variant}.service")
-
-    ${optionalString v4 ''
-    with subtest("Waiting for advertised IPv4 routes"):
-      host1.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.2\")) | any'")
-      host2.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.1\")) | any'")
-    ''}
-    ${optionalString v6 ''
-    with subtest("Waiting for advertised IPv6 routes"):
-      host1.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::2\")) | any'")
-      host2.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::1\")) | any'")
-    ''}
-
-    with subtest("Check fake routes in preCheckConfig do not exists"):
-      ${optionalString v4 ''host1.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")''}
-      ${optionalString v4 ''host2.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")''}
-
-      ${optionalString v6 ''host1.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")''}
-      ${optionalString v6 ''host2.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")''}
-  '';
-
-  makeBirdHost = variant: hostId: { pkgs, ... }: {
-    imports = [ (hostShared hostId) ];
-
-    services.${variant} = {
-      enable = true;
-
-      config = ''
-        log syslog all;
-
-        debug protocols all;
-
-        router id 10.0.0.${hostId};
-
-        protocol device {
-        }
-
-        protocol kernel {
-          import none;
-          export all;
-        }
-
-        protocol static {
-          include "static.conf";
-        }
-
-        protocol ospf {
-          export all;
-          area 0 {
-            interface "eth1" {
-              hello 5;
-              wait 5;
-            };
-          };
-        }
-      '';
-
-      preCheckConfig =
-        let
-          route = { bird = "1.2.3.4/32"; bird6 = "fd00::/128"; }.${variant};
-        in
-        ''echo "route ${route} blackhole;" > static.conf'';
-    };
-
-    systemd.tmpfiles.rules =
-      let
-        route = { bird = "10.10.0.${hostId}/32"; bird6 = "fdff::${hostId}/128"; }.${variant};
-      in
-      [ "f /etc/bird/static.conf - - - - route ${route} blackhole;" ];
-  };
-
-  makeBird2Host = hostId: { pkgs, ... }: {
-    imports = [ (hostShared hostId) ];
 
     services.bird2 = {
       enable = true;
@@ -198,8 +99,31 @@ let
     ];
   };
 in
-{
-  bird = birdTest true;
-  bird6 = birdTest false;
-  bird2 = bird2Test;
+makeTest {
+  name = "bird2";
+
+  nodes.host1 = makeBird2Host "1";
+  nodes.host2 = makeBird2Host "2";
+
+  testScript = ''
+    start_all()
+
+    host1.wait_for_unit("bird2.service")
+    host2.wait_for_unit("bird2.service")
+    host1.succeed("systemctl reload bird2.service")
+
+    with subtest("Waiting for advertised IPv4 routes"):
+      host1.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.2\")) | any'")
+      host2.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.1\")) | any'")
+    with subtest("Waiting for advertised IPv6 routes"):
+      host1.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::2\")) | any'")
+      host2.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::1\")) | any'")
+
+    with subtest("Check fake routes in preCheckConfig do not exists"):
+      host1.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
+      host2.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
+
+      host1.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
+      host2.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
+  '';
 }
diff --git a/nixos/tests/bitcoind.nix b/nixos/tests/bitcoind.nix
index 3e9e085287ac0..04655b7f6a51b 100644
--- a/nixos/tests/bitcoind.nix
+++ b/nixos/tests/bitcoind.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = with maintainers; [ _1000101 ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.bitcoind."mainnet" = {
       enable = true;
       rpc = {
diff --git a/nixos/tests/blockbook-frontend.nix b/nixos/tests/blockbook-frontend.nix
index e17a2d0577977..dca4f2f53cc10 100644
--- a/nixos/tests/blockbook-frontend.nix
+++ b/nixos/tests/blockbook-frontend.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = with maintainers; [ _1000101 ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.blockbook-frontend."test" = {
       enable = true;
     };
diff --git a/nixos/tests/blocky.nix b/nixos/tests/blocky.nix
new file mode 100644
index 0000000000000..18e7f45e1c738
--- /dev/null
+++ b/nixos/tests/blocky.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix {
+  name = "blocky";
+
+  nodes = {
+    server = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.dnsutils ];
+      services.blocky = {
+        enable = true;
+
+        settings = {
+          customDNS = {
+            mapping = {
+              "printer.lan" = "192.168.178.3,2001:0db8:85a3:08d3:1319:8a2e:0370:7344";
+            };
+          };
+          upstream = {
+            default = [ "8.8.8.8" "1.1.1.1" ];
+          };
+          port = 53;
+          httpPort = 5000;
+          logLevel = "info";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    with subtest("Service test"):
+        server.wait_for_unit("blocky.service")
+        server.wait_for_open_port(53)
+        server.wait_for_open_port(5000)
+        server.succeed("dig @127.0.0.1 +short -x 192.168.178.3 | grep -qF printer.lan")
+  '';
+}
diff --git a/nixos/tests/boot-stage1.nix b/nixos/tests/boot-stage1.nix
index 756decd2039d6..fbe82d61afaea 100644
--- a/nixos/tests/boot-stage1.nix
+++ b/nixos/tests/boot-stage1.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "boot-stage1";
 
-  machine = { config, pkgs, lib, ... }: {
+  nodes.machine = { config, pkgs, lib, ... }: {
     boot.extraModulePackages = let
       compileKernelModule = name: source: pkgs.runCommandCC name rec {
         inherit source;
diff --git a/nixos/tests/boot.nix b/nixos/tests/boot.nix
index cf55656671318..ec2a9f6527c93 100644
--- a/nixos/tests/boot.nix
+++ b/nixos/tests/boot.nix
@@ -38,7 +38,6 @@ let
       } // extraConfig);
     in
       makeTest {
-        inherit iso;
         name = "boot-" + name;
         nodes = { };
         testScript =
diff --git a/nixos/tests/botamusique.nix b/nixos/tests/botamusique.nix
index ccb105dc142f2..ecb79cb69867e 100644
--- a/nixos/tests/botamusique.nix
+++ b/nixos/tests/botamusique.nix
@@ -6,6 +6,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
 
   nodes = {
     machine = { config, ... }: {
+      networking.extraHosts = ''
+        127.0.0.1 all.api.radio-browser.info
+      '';
+
       services.murmur = {
         enable = true;
         registerName = "NixOS tests";
diff --git a/nixos/tests/bpf.nix b/nixos/tests/bpf.nix
index 233c7dab1ee22..5868e3bfcb4cc 100644
--- a/nixos/tests/bpf.nix
+++ b/nixos/tests/bpf.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "bpf";
   meta.maintainers = with pkgs.lib.maintainers; [ martinetd ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     programs.bcc.enable = true;
     environment.systemPackages = with pkgs; [ bpftrace ];
   };
@@ -18,8 +18,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     # simple BEGIN probe (user probe on bpftrace itself)
     print(machine.succeed("bpftrace -e 'BEGIN { print(\"ok\"); exit(); }'"))
     # tracepoint
-    print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit(); }'"))
+    print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit() }'"))
     # kprobe
     print(machine.succeed("bpftrace -e 'kprobe:schedule { print(probe); exit() }'"))
+    # BTF
+    print(machine.succeed("bpftrace -e 'kprobe:schedule { "
+        "    printf(\"tgid: %d\", ((struct task_struct*) curtask)->tgid); exit() "
+        "}'"))
   '';
 })
diff --git a/nixos/tests/breitbandmessung.nix b/nixos/tests/breitbandmessung.nix
index 12b1a094839be..78df0d5017eb0 100644
--- a/nixos/tests/breitbandmessung.nix
+++ b/nixos/tests/breitbandmessung.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ lib, ... }: {
   name = "breitbandmessung";
   meta.maintainers = with lib.maintainers; [ b4dm4n ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [
       ./common/user-account.nix
       ./common/x11.nix
diff --git a/nixos/tests/brscan5.nix b/nixos/tests/brscan5.nix
index 9aed742f6de79..9156a4cccfcfa 100644
--- a/nixos/tests/brscan5.nix
+++ b/nixos/tests/brscan5.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ mattchrist ];
   };
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
     {
       nixpkgs.config.allowUnfree = true;
       hardware.sane = {
diff --git a/nixos/tests/btrbk-no-timer.nix b/nixos/tests/btrbk-no-timer.nix
new file mode 100644
index 0000000000000..4fcab8839c892
--- /dev/null
+++ b/nixos/tests/btrbk-no-timer.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+  {
+    name = "btrbk-no-timer";
+    meta.maintainers = with lib.maintainers; [ oxalica ];
+
+    nodes.machine = { ... }: {
+      environment.systemPackages = with pkgs; [ btrfs-progs ];
+      services.btrbk.instances.local = {
+        onCalendar = null;
+        settings.volume."/mnt" = {
+          snapshot_dir = "btrbk/local";
+          subvolume = "to_backup";
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      # Create btrfs partition at /mnt
+      machine.succeed("truncate --size=128M /data_fs")
+      machine.succeed("mkfs.btrfs /data_fs")
+      machine.succeed("mkdir /mnt")
+      machine.succeed("mount /data_fs /mnt")
+      machine.succeed("btrfs subvolume create /mnt/to_backup")
+      machine.succeed("mkdir -p /mnt/btrbk/local")
+
+      # The service should not have any triggering timer.
+      unit = machine.get_unit_info('btrbk-local.service')
+      assert "TriggeredBy" not in unit
+
+      # Manually starting the service should still work.
+      machine.succeed("echo foo > /mnt/to_backup/bar")
+      machine.start_job("btrbk-local.service")
+      machine.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo")
+    '';
+  })
diff --git a/nixos/tests/buildkite-agents.nix b/nixos/tests/buildkite-agents.nix
index 6674a0e884ed3..2c5593323e873 100644
--- a/nixos/tests/buildkite-agents.nix
+++ b/nixos/tests/buildkite-agents.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     maintainers = [ flokli ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.buildkite-agents = {
       one = {
         privateSshKeyPath = (import ./ssh-keys.nix pkgs).snakeOilPrivateKey;
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index 0902904b20860..c4abd33d0395d 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   nodes = {
     webserver = { pkgs, lib, ... }: {
       services.caddy.enable = true;
-      services.caddy.config = ''
+      services.caddy.extraConfig = ''
         http://localhost {
           encode gzip
 
@@ -22,7 +22,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       '';
 
       specialisation.etag.configuration = {
-        services.caddy.config = lib.mkForce ''
+        services.caddy.extraConfig = lib.mkForce ''
           http://localhost {
             encode gzip
 
@@ -38,7 +38,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       };
 
       specialisation.config-reload.configuration = {
-        services.caddy.config = ''
+        services.caddy.extraConfig = ''
           http://localhost:8080 {
           }
         '';
@@ -61,7 +61,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     ''
       url = "http://localhost/example.html"
       webserver.wait_for_unit("caddy")
-      webserver.wait_for_open_port("80")
+      webserver.wait_for_open_port(80)
 
 
       def check_etag(url):
@@ -95,13 +95,13 @@ import ./make-test-python.nix ({ pkgs, ... }: {
           webserver.succeed(
               "${justReloadSystem}/bin/switch-to-configuration test >&2"
           )
-          webserver.wait_for_open_port("8080")
+          webserver.wait_for_open_port(8080)
 
       with subtest("multiple configs are correctly merged"):
           webserver.succeed(
               "${multipleConfigs}/bin/switch-to-configuration test >&2"
           )
-          webserver.wait_for_open_port("8080")
-          webserver.wait_for_open_port("8081")
+          webserver.wait_for_open_port(8080)
+          webserver.wait_for_open_port(8081)
     '';
 })
diff --git a/nixos/tests/cage.nix b/nixos/tests/cage.nix
index 83bae3deeeab2..39c8d0441b6d1 100644
--- a/nixos/tests/cage.nix
+++ b/nixos/tests/cage.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     maintainers = [ matthewbauer ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [ ./common/user-account.nix ];
diff --git a/nixos/tests/cagebreak.nix b/nixos/tests/cagebreak.nix
index c6c2c632b61ac..1dcc910f97444 100644
--- a/nixos/tests/cagebreak.nix
+++ b/nixos/tests/cagebreak.nix
@@ -13,7 +13,7 @@ in
     maintainers = [ berbiche ];
   };
 
-  machine = { config, ... }:
+  nodes.machine = { config, ... }:
   let
     alice = config.users.users.alice;
   in {
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index 29e7c279d69ac..556546beee764 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -48,7 +48,7 @@ let
       sudo
       ceph
       xfsprogs
-      netcat-openbsd
+      libressl.nc
     ];
 
     boot.kernelModules = [ "xfs" ];
diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix
index 170f09d9b76cc..e673df3131f8e 100644
--- a/nixos/tests/cfssl.nix
+++ b/nixos/tests/cfssl.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cfssl";
 
-  machine = { config, lib, pkgs, ... }:
+  nodes.machine = { config, lib, pkgs, ... }:
   {
     networking.firewall.allowedTCPPorts = [ config.services.cfssl.port ];
 
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
index 8965646bc5dcf..6b296fe8a61ae 100644
--- a/nixos/tests/chromium.nix
+++ b/nixos/tests/chromium.nix
@@ -15,26 +15,9 @@
 with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
-mapAttrs (channel: chromiumPkg: makeTest rec {
-  name = "chromium-${channel}";
-  meta = {
-    maintainers = with maintainers; [ aszlig primeos ];
-    # https://github.com/NixOS/hydra/issues/591#issuecomment-435125621
-    inherit (chromiumPkg.meta) timeout;
-  };
-
-  enableOCR = true;
-
+let
   user = "alice";
 
-  machine.imports = [ ./common/user-account.nix ./common/x11.nix ];
-  machine.virtualisation.memorySize = 2047;
-  machine.test-support.displayManager.auto.user = user;
-  machine.environment = {
-    systemPackages = [ chromiumPkg ];
-    variables."XAUTHORITY" = "/home/alice/.Xauthority";
-  };
-
   startupHTML = pkgs.writeText "chromium-startup.html" ''
     <!DOCTYPE html>
     <html>
@@ -50,6 +33,27 @@ mapAttrs (channel: chromiumPkg: makeTest rec {
     </body>
     </html>
   '';
+in
+
+mapAttrs (channel: chromiumPkg: makeTest {
+  name = "chromium-${channel}";
+  meta = {
+    maintainers = with maintainers; [ aszlig primeos ];
+    # https://github.com/NixOS/hydra/issues/591#issuecomment-435125621
+    inherit (chromiumPkg.meta) timeout;
+  };
+
+  enableOCR = true;
+
+  nodes.machine = { ... }: {
+    imports = [ ./common/user-account.nix ./common/x11.nix ];
+    virtualisation.memorySize = 2047;
+    test-support.displayManager.auto.user = user;
+    environment = {
+      systemPackages = [ chromiumPkg ];
+      variables."XAUTHORITY" = "/home/alice/.Xauthority";
+    };
+  };
 
   testScript = let
     xdo = name: text: let
diff --git a/nixos/tests/clickhouse.nix b/nixos/tests/clickhouse.nix
index 017f2ee35dab0..043263ec05dd9 100644
--- a/nixos/tests/clickhouse.nix
+++ b/nixos/tests/clickhouse.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "clickhouse";
   meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
 
-  machine = {
+  nodes.machine = {
     services.clickhouse.enable = true;
     virtualisation.memorySize = 4096;
   };
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index 3f191ff5616e0..9feb8d5c1526e 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -61,7 +61,7 @@ in makeTest {
   meta = with pkgs.lib.maintainers; {
     maintainers = [ lewo ];
   };
-  machine = { ... }:
+  nodes.machine = { ... }:
   {
     virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
     services.cloud-init = {
diff --git a/nixos/tests/cntr.nix b/nixos/tests/cntr.nix
index 668470756209a..598143beb6c0f 100644
--- a/nixos/tests/cntr.nix
+++ b/nixos/tests/cntr.nix
@@ -28,10 +28,16 @@ let
       testScript = ''
         start_all()
         ${backend}.wait_for_unit("${backend}-nginx.service")
-        result = ${backend}.wait_until_succeeds(
-            "cntr attach -t ${backend} nginx sh -- -c 'curl localhost | grep Hello'"
+        ${backend}.wait_for_open_port(8181)
+        # For some reason, the cntr command hangs when run without the &.
+        # As such, we have to do some messy things to ensure we check the exitcode and output in a race-condition-safe manner
+        ${backend}.execute(
+            "(cntr attach -t ${backend} nginx sh -- -c 'curl localhost | grep Hello' > /tmp/result; echo $? > /tmp/exitcode; touch /tmp/done) &"
         )
-        assert "Hello" in result
+
+        ${backend}.wait_for_file("/tmp/done")
+        assert "0" == ${backend}.succeed("cat /tmp/exitcode").strip(), "non-zero exit code"
+        assert "Hello" in ${backend}.succeed("cat /tmp/result"), "no greeting in output"
       '';
     };
 
@@ -40,7 +46,7 @@ let
 
     meta = with pkgs.lib.maintainers; { maintainers = [ sorki mic92 ]; };
 
-    machine = { lib, ... }: {
+    nodes.machine = { lib, ... }: {
       environment.systemPackages = [ pkgs.cntr ];
       containers.test = {
         autoStart = true;
@@ -54,7 +60,13 @@ let
     testScript = ''
       machine.start()
       machine.wait_for_unit("container@test.service")
-      machine.succeed("cntr attach test sh -- -c 'ping -c5 172.16.0.1'")
+      # I haven't observed the same hanging behaviour in this version as in the OCI version which necessetates this messy invocation, but it's probably better to be safe than sorry and use it here as well
+      machine.execute(
+          "(cntr attach test sh -- -c 'ping -c5 172.16.0.1'; echo $? > /tmp/exitcode; touch /tmp/done) &"
+      )
+
+      machine.wait_for_file("/tmp/done")
+      assert "0" == machine.succeed("cat /tmp/exitcode").strip(), "non-zero exit code"
     '';
   };
 in {
diff --git a/nixos/tests/collectd.nix b/nixos/tests/collectd.nix
index cb196224a2317..2480bdb5f917e 100644
--- a/nixos/tests/collectd.nix
+++ b/nixos/tests/collectd.nix
@@ -2,12 +2,15 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "collectd";
   meta = { };
 
-  machine =
-    { pkgs, ... }:
+  nodes.machine =
+    { pkgs, lib, ... }:
 
     {
       services.collectd = {
         enable = true;
+        extraConfig = lib.mkBefore ''
+          Interval 30
+        '';
         plugins = {
           rrdtool = ''
             DataDir "/var/lib/collectd/rrd"
@@ -26,6 +29,8 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     machine.succeed(f"rrdinfo {file} | logger")
     # check that this file contains a shortterm metric
     machine.succeed(f"rrdinfo {file} | grep -F 'ds[shortterm].min = '")
+    # check that interval was set before the plugins
+    machine.succeed(f"rrdinfo {file} | grep -F 'step = 30'")
     # check that there are frequent updates
     machine.succeed(f"cp {file} before")
     machine.wait_until_fails(f"cmp before {file}")
diff --git a/nixos/tests/common/lxd/config.yaml b/nixos/tests/common/lxd/config.yaml
new file mode 100644
index 0000000000000..3bb667ed43f7c
--- /dev/null
+++ b/nixos/tests/common/lxd/config.yaml
@@ -0,0 +1,24 @@
+storage_pools:
+  - name: default
+    driver: dir
+    config:
+      source: /var/lxd-pool
+
+networks:
+  - name: lxdbr0
+    type: bridge
+    config:
+      ipv4.address: auto
+      ipv6.address: none
+
+profiles:
+  - name: default
+    devices:
+      eth0:
+        name: eth0
+        network: lxdbr0
+        type: nic
+      root:
+        path: /
+        pool: default
+        type: disk
diff --git a/nixos/tests/containers-bridge.nix b/nixos/tests/containers-bridge.nix
index b8661fd7997c9..d2e16299edaad 100644
--- a/nixos/tests/containers-bridge.nix
+++ b/nixos/tests/containers-bridge.nix
@@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     { imports = [ ../modules/installer/cd-dvd/channel.nix ];
       virtualisation.writableStore = true;
diff --git a/nixos/tests/containers-custom-pkgs.nix b/nixos/tests/containers-custom-pkgs.nix
index 1627a2c70c3ca..e8740ac631345 100644
--- a/nixos/tests/containers-custom-pkgs.nix
+++ b/nixos/tests/containers-custom-pkgs.nix
@@ -9,10 +9,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: let
 in {
   name = "containers-custom-pkgs";
   meta = {
-    maintainers = with lib.maintainers; [ adisbladis earvstedt ];
+    maintainers = with lib.maintainers; [ adisbladis erikarvstedt ];
   };
 
-  machine = { config, ... }: {
+  nodes.machine = { config, ... }: {
     assertions = let
       helloName = (builtins.head config.containers.test.config.system.extraDependencies).name;
     in [ {
diff --git a/nixos/tests/containers-ephemeral.nix b/nixos/tests/containers-ephemeral.nix
index db1631cf5b5d1..cb4b7d4eba0fd 100644
--- a/nixos/tests/containers-ephemeral.nix
+++ b/nixos/tests/containers-ephemeral.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ patryk27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.writableStore = true;
 
     containers.webserver = {
@@ -33,10 +33,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     machine.succeed("nixos-container start webserver")
 
     with subtest("Container got its own root folder"):
-        machine.succeed("ls /run/containers/webserver")
+        machine.succeed("ls /run/nixos-containers/webserver")
 
     with subtest("Container persistent directory is not created"):
-        machine.fail("ls /var/lib/containers/webserver")
+        machine.fail("ls /var/lib/nixos-containers/webserver")
 
     # Since "start" returns after the container has reached
     # multi-user.target, we should now be able to access it.
@@ -49,6 +49,6 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
         machine.fail(f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null")
 
     with subtest("Container's root folder was removed"):
-        machine.fail("ls /run/containers/webserver")
+        machine.fail("ls /run/nixos-containers/webserver")
   '';
 })
diff --git a/nixos/tests/containers-extra_veth.nix b/nixos/tests/containers-extra_veth.nix
index b8f3d9844064c..f3e62265f6c4f 100644
--- a/nixos/tests/containers-extra_veth.nix
+++ b/nixos/tests/containers-extra_veth.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ kampfschlaefer ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     { imports = [ ../modules/installer/cd-dvd/channel.nix ];
       virtualisation.writableStore = true;
diff --git a/nixos/tests/containers-hosts.nix b/nixos/tests/containers-hosts.nix
index 3c6a15710027a..7bce7c997efee 100644
--- a/nixos/tests/containers-hosts.nix
+++ b/nixos/tests/containers-hosts.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ montag451 ];
   };
 
-  machine =
+  nodes.machine =
     { lib, ... }:
     {
       virtualisation.vlans = [];
diff --git a/nixos/tests/containers-imperative.nix b/nixos/tests/containers-imperative.nix
index 14001657bee0b..3007efaf88710 100644
--- a/nixos/tests/containers-imperative.nix
+++ b/nixos/tests/containers-imperative.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
   };
 
-  machine =
+  nodes.machine =
     { config, pkgs, lib, ... }:
     { imports = [ ../modules/installer/cd-dvd/channel.nix ];
 
@@ -18,8 +18,9 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
       # container available within the VM, because we don't have network access.
       virtualisation.additionalPaths = let
         emptyContainer = import ../lib/eval-config.nix {
-          inherit (config.nixpkgs.localSystem) system;
           modules = lib.singleton {
+            nixpkgs = { inherit (config.nixpkgs) localSystem; };
+
             containers.foo.config = {
               system.stateVersion = "18.03";
             };
@@ -69,8 +70,8 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
 
       with subtest(f"Put the root of {id2} into a bind mount"):
           machine.succeed(
-              f"mv /var/lib/containers/{id2} /id2-bindmount",
-              f"mount --bind /id2-bindmount /var/lib/containers/{id1}",
+              f"mv /var/lib/nixos-containers/{id2} /id2-bindmount",
+              f"mount --bind /id2-bindmount /var/lib/nixos-containers/{id1}",
           )
 
           ip1 = machine.succeed(f"nixos-container show-ip {id1}").rstrip()
@@ -88,7 +89,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
           "Create a directory with a dummy file and bind-mount it into both containers."
       ):
           for id in id1, id2:
-              important_path = f"/var/lib/containers/{id}/very/important/data"
+              important_path = f"/var/lib/nixos-containers/{id}/very/important/data"
               machine.succeed(
                   f"mkdir -p {important_path}",
                   f"mount --bind /nested-bindmount {important_path}",
@@ -154,13 +155,13 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
           machine.succeed("grep -qF 'important data' /nested-bindmount/dummy")
 
       with subtest("Ensure that the container path is gone"):
-          print(machine.succeed("ls -lsa /var/lib/containers"))
-          machine.succeed(f"test ! -e /var/lib/containers/{id1}")
+          print(machine.succeed("ls -lsa /var/lib/nixos-containers"))
+          machine.succeed(f"test ! -e /var/lib/nixos-containers/{id1}")
 
       with subtest("Ensure that a failed container creation doesn'leave any state"):
           machine.fail(
               "nixos-container create b0rk --config-file ${brokenCfg}"
           )
-          machine.succeed("test ! -e /var/lib/containers/b0rk")
+          machine.succeed("test ! -e /var/lib/nixos-containers/b0rk")
     '';
 })
diff --git a/nixos/tests/containers-ip.nix b/nixos/tests/containers-ip.nix
index 91fdda0392a9b..ecead5c22f756 100644
--- a/nixos/tests/containers-ip.nix
+++ b/nixos/tests/containers-ip.nix
@@ -17,7 +17,7 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }: {
       imports = [ ../modules/installer/cd-dvd/channel.nix ];
       virtualisation = {
diff --git a/nixos/tests/containers-names.nix b/nixos/tests/containers-names.nix
index 9ad2bfb748a8d..721f64990724e 100644
--- a/nixos/tests/containers-names.nix
+++ b/nixos/tests/containers-names.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ patryk27 ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     # We're using the newest kernel, so that we can test containers with long names.
     # Please see https://github.com/NixOS/nixpkgs/issues/38509 for details.
     boot.kernelPackages = pkgs.linuxPackages_latest;
diff --git a/nixos/tests/containers-nested.nix b/nixos/tests/containers-nested.nix
index a653361494f96..4a9fb8f01e241 100644
--- a/nixos/tests/containers-nested.nix
+++ b/nixos/tests/containers-nested.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   meta = with pkgs.lib.maintainers; { maintainers = [ sorki ]; };
 
-  machine = { lib, ... }:
+  nodes.machine = { lib, ... }:
     let
       makeNested = subConf: {
         containers.nested = {
diff --git a/nixos/tests/containers-portforward.nix b/nixos/tests/containers-portforward.nix
index 6cecd72f1bda3..b8c7aabc5a50b 100644
--- a/nixos/tests/containers-portforward.nix
+++ b/nixos/tests/containers-portforward.nix
@@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ianwookim ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     { imports = [ ../modules/installer/cd-dvd/channel.nix ];
       virtualisation.writableStore = true;
diff --git a/nixos/tests/containers-tmpfs.nix b/nixos/tests/containers-tmpfs.nix
index d95178d1ff588..cf5b81656afef 100644
--- a/nixos/tests/containers-tmpfs.nix
+++ b/nixos/tests/containers-tmpfs.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ patryk27 ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     { imports = [ ../modules/installer/cd-dvd/channel.nix ];
       virtualisation.writableStore = true;
@@ -62,7 +62,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
           machine.succeed(
               tmpfs_cmd("touch /root/test.file"),
               tmpfs_cmd("ls -l  /root | grep -q test.file"),
-              "test -e /var/lib/containers/tmpfs/root/test.file",
+              "test -e /var/lib/nixos-containers/tmpfs/root/test.file",
           )
 
       with subtest(
@@ -73,7 +73,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
               tmpfs_cmd("touch /some/random/path/test.file"),
               tmpfs_cmd("test -e /some/random/path/test.file"),
           )
-          machine.fail("test -e /var/lib/containers/tmpfs/some/random/path/test.file")
+          machine.fail("test -e /var/lib/nixos-containers/tmpfs/some/random/path/test.file")
 
       with subtest(
           "files created in the hosts container dir in a path where a tmpfs "
@@ -81,9 +81,9 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
           + "the do not exist in the tmpfs"
       ):
           machine.succeed(
-              "touch /var/lib/containers/tmpfs/var/test.file",
-              "test -e /var/lib/containers/tmpfs/var/test.file",
-              "ls -l /var/lib/containers/tmpfs/var/ | grep -q test.file 2>/dev/null",
+              "touch /var/lib/nixos-containers/tmpfs/var/test.file",
+              "test -e /var/lib/nixos-containers/tmpfs/var/test.file",
+              "ls -l /var/lib/nixos-containers/tmpfs/var/ | grep -q test.file 2>/dev/null",
           )
           machine.fail(tmpfs_cmd("ls -l /var | grep -q test.file"))
     '';
diff --git a/nixos/tests/convos.nix b/nixos/tests/convos.nix
index a13870d170843..cc0c2e75893c1 100644
--- a/nixos/tests/convos.nix
+++ b/nixos/tests/convos.nix
@@ -23,7 +23,7 @@ in
 
   testScript = ''
     machine.wait_for_unit("convos")
-    machine.wait_for_open_port("${toString port}")
+    machine.wait_for_open_port(${toString port})
     machine.succeed("journalctl -u convos | grep -q 'Listening at.*${toString port}'")
     machine.succeed("curl -f http://localhost:${toString port}/")
   '';
diff --git a/nixos/tests/cri-o.nix b/nixos/tests/cri-o.nix
index 91d46657f2411..d3a8713d6a9b3 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.lib.maintainers; teams.podman.members;
+  meta.maintainers = with pkgs.lib.maintainers; teams.podman.members;
 
   nodes = {
     crio = {
diff --git a/nixos/tests/cryptpad.nix b/nixos/tests/cryptpad.nix
deleted file mode 100644
index 895f291abaca3..0000000000000
--- a/nixos/tests/cryptpad.nix
+++ /dev/null
@@ -1,18 +0,0 @@
-import ./make-test-python.nix ({ lib, ... }:
-
-with lib;
-
-{
-  name = "cryptpad";
-  meta.maintainers = with maintainers; [ davhau ];
-
-  nodes.machine =
-    { pkgs, ... }:
-    { services.cryptpad.enable = true; };
-
-  testScript = ''
-    machine.wait_for_unit("cryptpad.service")
-    machine.wait_for_open_port("3000")
-    machine.succeed("curl -L --fail http://localhost:3000/sheet")
-  '';
-})
diff --git a/nixos/tests/custom-ca.nix b/nixos/tests/custom-ca.nix
index a55449a397a7c..73e47c3c9d0d6 100644
--- a/nixos/tests/custom-ca.nix
+++ b/nixos/tests/custom-ca.nix
@@ -1,11 +1,18 @@
 # 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.
+# engines: Gecko (via Firefox), Chromium, QtWebEngine (via qutebrowser) 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, ... }:
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
 
 let
+  inherit (pkgs) lib;
+
   makeCert = { caName, domain }: pkgs.runCommand "example-cert"
   { buildInputs = [ pkgs.gnutls ]; }
   ''
@@ -68,24 +75,8 @@ let
       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 = 600;
-
-      networking.hosts."127.0.0.1" = [ "good.example.com" "bad.example.com" ];
+  webserverConfig =
+    { networking.hosts."127.0.0.1" = [ "good.example.com" "bad.example.com" ];
       security.pki.certificateFiles = [ "${example-good-cert}/ca.crt" ];
 
       services.nginx.enable = true;
@@ -107,73 +98,98 @@ in
             return 200 'It does not work!';
           '';
         };
-
-      environment.systemPackages = with pkgs; [
-        xdotool
-        firefox
-        chromium
-        qutebrowser
-        midori
-      ];
     };
 
-  testScript = ''
-    from typing import Tuple
-    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": "Security Risk",
-      "chromium": "not private",
-      "qutebrowser -T": "Certificate error",
-      "midori": "Security"
-    }
-
-    machine.wait_for_x()
-    for command, error in browsers.items():
-        browser = command.split()[0]
-        with subtest("Good certificate is trusted in " + browser):
-            execute_as(
-                "alice", f"{command} 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"{command} https://bad.example.com >&2 &")
-            machine.wait_for_text(error)
-            machine.screenshot("bad" + browser)
-            machine.succeed("pkill -f " + browser)
-  '';
-})
+  curlTest = makeTest {
+    name = "custom-ca-curl";
+    meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+    nodes.machine = { ... }: webserverConfig;
+    testScript = ''
+        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")
+    '';
+  };
+
+  mkBrowserTest = browser: testParams: makeTest {
+    name = "custom-ca-${browser}";
+    meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+    enableOCR = true;
+
+    nodes.machine = { pkgs, ... }:
+      { imports =
+          [ ./common/user-account.nix
+            ./common/x11.nix
+            webserverConfig
+          ];
+
+        # chromium-based browsers refuse to run as root
+        test-support.displayManager.auto.user = "alice";
+
+        # browsers may hang with the default memory
+        virtualisation.memorySize = 600;
+
+        environment.systemPackages = [ pkgs.xdotool pkgs.${browser} ];
+      };
+
+    testScript = ''
+      from typing import Tuple
+      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()
+      machine.wait_for_x()
+
+      command = "${browser} ${testParams.args or ""}"
+      with subtest("Good certificate is trusted in ${browser}"):
+          execute_as(
+              "alice", f"{command} https://good.example.com >&2 &"
+          )
+          wait_for_window_as("alice", "${browser}")
+          machine.sleep(4)
+          execute_as("alice", "xdotool key ctrl+r")  # reload to be safe
+          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"{command} https://bad.example.com >&2 &")
+          machine.wait_for_text("${testParams.error}")
+          machine.screenshot("bad${browser}")
+    '';
+  };
+
+in
+
+{
+  curl = curlTest;
+} // pkgs.lib.mapAttrs mkBrowserTest {
+  firefox = { error = "Security Risk"; };
+  chromium = { error = "not private"; };
+  qutebrowser = { args = "-T"; error = "Certificate error"; };
+  midori = { error = "Security"; };
+}
diff --git a/nixos/tests/deluge.nix b/nixos/tests/deluge.nix
index 33c57ce7c36c8..0cd1d21870adf 100644
--- a/nixos/tests/deluge.nix
+++ b/nixos/tests/deluge.nix
@@ -47,7 +47,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     simple.wait_for_unit("deluged")
     simple.wait_for_unit("delugeweb")
-    simple.wait_for_open_port("8112")
+    simple.wait_for_open_port(8112)
     declarative.wait_for_unit("network.target")
     declarative.wait_until_succeeds("curl --fail http://simple:8112")
 
diff --git a/nixos/tests/disable-installer-tools.nix b/nixos/tests/disable-installer-tools.nix
index 23c15faa8d334..69f99122753a8 100644
--- a/nixos/tests/disable-installer-tools.nix
+++ b/nixos/tests/disable-installer-tools.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
 {
   name = "disable-installer-tools";
 
-  machine =
+  nodes.machine =
     { pkgs, lib, ... }:
     {
         system.disableInstallerTools = true;
diff --git a/nixos/tests/dnsdist.nix b/nixos/tests/dnsdist.nix
index cfc41c13864e0..e72fa05ff282b 100644
--- a/nixos/tests/dnsdist.nix
+++ b/nixos/tests/dnsdist.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix (
       maintainers = with maintainers; [ jojosch ];
     };
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       services.bind = {
         enable = true;
         extraOptions = "empty-zones-enable no;";
diff --git a/nixos/tests/doas.nix b/nixos/tests/doas.nix
index 7f038b2bee296..3713c728195ce 100644
--- a/nixos/tests/doas.nix
+++ b/nixos/tests/doas.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix (
       maintainers = [ cole-h ];
     };
 
-    machine =
+    nodes.machine =
       { ... }:
         {
           users.groups = { foobar = {}; barfoo = {}; baz = { gid = 1337; }; };
diff --git a/nixos/tests/docker-edge.nix b/nixos/tests/docker-edge.nix
deleted file mode 100644
index c6a1a08301890..0000000000000
--- a/nixos/tests/docker-edge.nix
+++ /dev/null
@@ -1,49 +0,0 @@
-# This test runs docker and checks if simple container starts
-
-import ./make-test-python.nix ({ pkgs, ...} : {
-  name = "docker";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ nequissimus offline ];
-  };
-
-  nodes = {
-    docker =
-      { pkgs, ... }:
-        {
-          virtualisation.docker.enable = true;
-          virtualisation.docker.package = pkgs.docker-edge;
-
-          users.users = {
-            noprivs = {
-              isNormalUser = true;
-              description = "Can't access the docker daemon";
-              password = "foobar";
-            };
-
-            hasprivs = {
-              isNormalUser = true;
-              description = "Can access the docker daemon";
-              password = "foobar";
-              extraGroups = [ "docker" ];
-            };
-          };
-        };
-    };
-
-  testScript = ''
-    start_all()
-
-    docker.wait_for_unit("sockets.target")
-    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
-    docker.succeed(
-        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
-    )
-    docker.succeed("docker ps | grep sleeping")
-    docker.succeed("sudo -u hasprivs docker ps")
-    docker.fail("sudo -u noprivs docker ps")
-    docker.succeed("docker stop sleeping")
-
-    # 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 1d449db45191f..316b7c9b97273 100644
--- a/nixos/tests/docker-registry.nix
+++ b/nixos/tests/docker-registry.nix
@@ -35,7 +35,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     registry.start()
     registry.wait_for_unit("docker-registry.service")
-    registry.wait_for_open_port("8080")
+    registry.wait_for_open_port(8080)
     client1.succeed("docker push registry:8080/scratch")
 
     client2.start()
diff --git a/nixos/tests/docker-tools-cross.nix b/nixos/tests/docker-tools-cross.nix
index a7a6a31475d67..14cb14ceeaea9 100644
--- a/nixos/tests/docker-tools-cross.nix
+++ b/nixos/tests/docker-tools-cross.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 let
 
   remoteSystem =
-    if pkgs.system == "aarch64-linux"
+    if pkgs.stdenv.hostPlatform.system == "aarch64-linux"
     then "x86_64-linux"
     else "aarch64-linux";
 
@@ -18,13 +18,17 @@ let
 
     # 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;
+    crossSystem = pkgs.stdenv.hostPlatform.system;
   };
 
   hello1 = remoteCrossPkgs.dockerTools.buildImage {
     name = "hello1";
     tag = "latest";
-    contents = remoteCrossPkgs.hello;
+    copyToRoot = remoteCrossPkgs.buildEnv {
+      name = "image-root";
+      pathsToLink = [ "/bin" ];
+      paths = [ remoteCrossPkgs.hello ];
+    };
   };
 
   hello2 = remoteCrossPkgs.dockerTools.buildLayeredImage {
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 8a240ddb17f24..99a968f17af2f 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -315,7 +315,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
                 "docker inspect ${pkgs.dockerTools.examples.cross.imageName} "
                 + "| ${pkgs.jq}/bin/jq -r .[].Architecture"
             ).strip()
-            == "${if pkgs.system == "aarch64-linux" then "amd64" else "arm64"}"
+            == "${if pkgs.stdenv.hostPlatform.system == "aarch64-linux" then "amd64" else "arm64"}"
         )
 
     with subtest("buildLayeredImage doesn't dereference /nix/store symlink layers"):
@@ -419,5 +419,10 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker rmi layered-image-with-path",
         )
 
+    with subtest("etc"):
+        docker.succeed("${examples.etc} | docker load")
+        docker.succeed("docker run --rm etc | grep localhost")
+        docker.succeed("docker image rm etc:latest")
+
   '';
 })
diff --git a/nixos/tests/documize.nix b/nixos/tests/documize.nix
index d5a77ffcd4f26..528bf5338ce0d 100644
--- a/nixos/tests/documize.nix
+++ b/nixos/tests/documize.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     maintainers = [ ma27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.jq ];
 
     services.documize = {
diff --git a/nixos/tests/doh-proxy-rust.nix b/nixos/tests/doh-proxy-rust.nix
index 23f8616849c3d..11ed87d23bbe6 100644
--- a/nixos/tests/doh-proxy-rust.nix
+++ b/nixos/tests/doh-proxy-rust.nix
@@ -38,6 +38,6 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     machine.wait_for_unit("doh-proxy-rust.service")
     machine.wait_for_open_port(53)
     machine.wait_for_open_port(3000)
-    machine.succeed(f"curl --fail '{url}?dns={query}' | grep -F {bin_ip}")
+    machine.succeed(f"curl --fail -H 'Accept: application/dns-message' '{url}?dns={query}' | grep -F {bin_ip}")
   '';
 })
diff --git a/nixos/tests/domination.nix b/nixos/tests/domination.nix
index c76d4ed8c61b3..09027740ab8de 100644
--- a/nixos/tests/domination.nix
+++ b/nixos/tests/domination.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix
index 8913c2a6a7e82..5439387807fd7 100644
--- a/nixos/tests/dovecot.nix
+++ b/nixos/tests/dovecot.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "dovecot";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ common/user-account.nix ];
     services.postfix.enable = true;
     services.dovecot2 = {
diff --git a/nixos/tests/earlyoom.nix b/nixos/tests/earlyoom.nix
new file mode 100644
index 0000000000000..75bdf56899b30
--- /dev/null
+++ b/nixos/tests/earlyoom.nix
@@ -0,0 +1,16 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "earlyoom";
+  meta = {
+    maintainers = with lib.maintainers; [ ncfavier ];
+  };
+
+  machine = {
+    services.earlyoom = {
+      enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("earlyoom.service")
+  '';
+})
diff --git a/nixos/tests/ecryptfs.nix b/nixos/tests/ecryptfs.nix
index ef7bd13eb92c4..1c67d307a00e8 100644
--- a/nixos/tests/ecryptfs.nix
+++ b/nixos/tests/ecryptfs.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ ... }:
 {
   name = "ecryptfs";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ./common/user-account.nix ];
     boot.kernelModules = [ "ecryptfs" ];
     security.pam.enableEcryptfs = true;
@@ -11,16 +11,16 @@ import ./make-test-python.nix ({ ... }:
 
   testScript = ''
     def login_as_alice():
-        machine.wait_until_tty_matches(1, "login: ")
+        machine.wait_until_tty_matches("1", "login: ")
         machine.send_chars("alice\n")
-        machine.wait_until_tty_matches(1, "Password: ")
+        machine.wait_until_tty_matches("1", "Password: ")
         machine.send_chars("foobar\n")
-        machine.wait_until_tty_matches(1, "alice\@machine")
+        machine.wait_until_tty_matches("1", "alice\@machine")
 
 
     def logout():
         machine.send_chars("logout\n")
-        machine.wait_until_tty_matches(1, "login: ")
+        machine.wait_until_tty_matches("1", "login: ")
 
 
     machine.wait_for_unit("default.target")
@@ -36,7 +36,7 @@ import ./make-test-python.nix ({ ... }:
     with subtest("Log alice in (ecryptfs passwhrase is wrapped during first login)"):
         login_as_alice()
         machine.send_chars("logout\n")
-        machine.wait_until_tty_matches(1, "login: ")
+        machine.wait_until_tty_matches("1", "login: ")
 
     # Why do I need to do this??
     machine.succeed("su alice -c ecryptfs-umount-private || true")
diff --git a/nixos/tests/emacs-daemon.nix b/nixos/tests/emacs-daemon.nix
index e12da56021dab..310e93e19b0b4 100644
--- a/nixos/tests/emacs-daemon.nix
+++ b/nixos/tests/emacs-daemon.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
   enableOCR = true;
 
-  machine =
+  nodes.machine =
     { ... }:
 
     { imports = [ ./common/x11.nix ];
@@ -33,7 +33,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       )
 
       # connects to the daemon
-      machine.succeed("emacsclient --create-frame $EDITOR >&2 &")
+      machine.succeed("emacsclient --no-wait --frame-parameters='((display . \"'\"$DISPLAY\"'\"))' --create-frame $EDITOR >&2")
 
       # checks that Emacs shows the edited filename
       machine.wait_for_text("emacseditor")
diff --git a/nixos/tests/empty-file b/nixos/tests/empty-file
new file mode 100644
index 0000000000000..e69de29bb2d1d
--- /dev/null
+++ b/nixos/tests/empty-file
diff --git a/nixos/tests/enlightenment.nix b/nixos/tests/enlightenment.nix
index 8506c348246de..2e06eedd9915b 100644
--- a/nixos/tests/enlightenment.nix
+++ b/nixos/tests/enlightenment.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     maintainers = [ romildo ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
   {
     imports = [ ./common/user-account.nix ];
     services.xserver.enable = true;
diff --git a/nixos/tests/env.nix b/nixos/tests/env.nix
index fc96ace6b2d2a..dec17b6b565a0 100644
--- a/nixos/tests/env.nix
+++ b/nixos/tests/env.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ nequissimus ];
   };
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
     {
       boot.kernelPackages = pkgs.linuxPackages;
       environment.etc.plainFile.text = ''
diff --git a/nixos/tests/envoy.nix b/nixos/tests/envoy.nix
new file mode 100644
index 0000000000000..9d2c32ce102f2
--- /dev/null
+++ b/nixos/tests/envoy.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "envoy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ cameronnemo ];
+  };
+
+  nodes.machine = { pkgs, ... }: {
+    services.envoy.enable = true;
+    services.envoy.settings = {
+      admin = {
+        access_log_path = "/dev/null";
+        address = {
+          socket_address = {
+            protocol = "TCP";
+            address = "127.0.0.1";
+            port_value = 9901;
+          };
+        };
+      };
+      static_resources = {
+        listeners = [];
+        clusters = [];
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("envoy.service")
+    machine.wait_for_open_port(9901)
+    machine.wait_until_succeeds("curl -fsS localhost:9901/ready")
+  '';
+})
diff --git a/nixos/tests/etebase-server.nix b/nixos/tests/etebase-server.nix
index 4fc3c1f6392f8..49bfccf359e2a 100644
--- a/nixos/tests/etebase-server.nix
+++ b/nixos/tests/etebase-server.nix
@@ -9,7 +9,7 @@ in {
       maintainers = [ felschr ];
     };
 
-    machine = { pkgs, ... }:
+    nodes.machine = { pkgs, ... }:
       {
         services.etebase-server = {
           inherit dataDir;
diff --git a/nixos/tests/etesync-dav.nix b/nixos/tests/etesync-dav.nix
index 6a747e23f76f6..f49152c60991f 100644
--- a/nixos/tests/etesync-dav.nix
+++ b/nixos/tests/etesync-dav.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ _3699n ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
       environment.systemPackages = [ pkgs.curl pkgs.etesync-dav ];
   };
 
diff --git a/nixos/tests/extra-python-packages.nix b/nixos/tests/extra-python-packages.nix
new file mode 100644
index 0000000000000..7a48077cf98bc
--- /dev/null
+++ b/nixos/tests/extra-python-packages.nix
@@ -0,0 +1,13 @@
+import ./make-test-python.nix ({ ... }:
+  {
+    name = "extra-python-packages";
+
+    extraPythonPackages = p: [ p.numpy ];
+
+    nodes = { };
+
+    testScript = ''
+      import numpy as np
+      assert str(np.zeros(4) == "array([0., 0., 0., 0.])")
+    '';
+  })
diff --git a/nixos/tests/fancontrol.nix b/nixos/tests/fancontrol.nix
index 296c680264150..ecb9360974463 100644
--- a/nixos/tests/fancontrol.nix
+++ b/nixos/tests/fancontrol.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     maintainers = [ evils ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
     hardware.fancontrol.enable = true;
     hardware.fancontrol.config = ''
diff --git a/nixos/tests/fcitx/default.nix b/nixos/tests/fcitx/default.nix
index a243be8dc19b1..c132249fcb249 100644
--- a/nixos/tests/fcitx/default.nix
+++ b/nixos/tests/fcitx/default.nix
@@ -5,7 +5,8 @@ import ../make-test-python.nix (
     # copy_from_host works only for store paths
     rec {
         name = "fcitx";
-        machine =
+        meta.broken = true; # takes hours to time out since October 2021
+        nodes.machine =
         {
           pkgs,
           ...
diff --git a/nixos/tests/fenics.nix b/nixos/tests/fenics.nix
index f0a8c32c7cd8e..1d182cfc44998 100644
--- a/nixos/tests/fenics.nix
+++ b/nixos/tests/fenics.nix
@@ -44,6 +44,6 @@ in
     { nodes, ... }:
     ''
       start_all()
-      node1.succeed("${fenicsScript}")
+      fenicsnode.succeed("${fenicsScript}")
     '';
 })
diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix
index 6101fc9735641..9bb78a97f36d7 100644
--- a/nixos/tests/firefox.nix
+++ b/nixos/tests/firefox.nix
@@ -1,15 +1,24 @@
-import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }: {
-  name = "firefox";
+import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }:
+let firefoxPackage' = firefoxPackage.override (args: {
+      extraPrefsFiles = (args.extraPrefsFiles or []) ++ [
+        # make sure that autoplay is enabled by default for the audio test
+        (builtins.toString (builtins.toFile "autoplay-pref.js" ''defaultPref("media.autoplay.default",0);''))
+      ];
+  });
+
+in
+{
+  name = firefoxPackage'.unwrapped.binaryName;
   meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco shlevy ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
 
     { imports = [ ./common/x11.nix ];
       environment.systemPackages = [
-        firefoxPackage
+        firefoxPackage'
         pkgs.xdotool
       ];
 
@@ -54,7 +63,7 @@ import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }: {
 
 
       @contextmanager
-      def audio_recording(machine: Machine) -> None:
+      def record_audio(machine: Machine):
           """
           Perform actions while recording the
           machine audio output.
@@ -64,7 +73,7 @@ import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }: {
           machine.systemctl("stop audio-recorder")
 
 
-      def wait_for_sound(machine: Machine) -> None:
+      def wait_for_sound(machine: Machine):
           """
           Wait until any sound has been emitted.
           """
@@ -88,15 +97,15 @@ import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }: {
 
       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' >&2 &"
+              "xterm -e '${firefoxPackage'.unwrapped.binaryName} file://${pkgs.valgrind.doc}/share/doc/valgrind/html/index.html' >&2 &"
           )
           machine.wait_for_window("Valgrind")
           machine.sleep(40)
 
       with subtest("Check whether Firefox can play sound"):
-          with audio_recording(machine):
+          with record_audio(machine):
               machine.succeed(
-                  "firefox file://${pkgs.sound-theme-freedesktop}/share/sounds/freedesktop/stereo/phone-incoming-call.oga >&2 &"
+                  "${firefoxPackage'.unwrapped.binaryName} file://${pkgs.sound-theme-freedesktop}/share/sounds/freedesktop/stereo/phone-incoming-call.oga >&2 &"
               )
               wait_for_sound(machine)
           machine.copy_from_vm("/tmp/record.wav")
diff --git a/nixos/tests/fish.nix b/nixos/tests/fish.nix
index 68fba428439b6..3d9b13c6af70a 100644
--- a/nixos/tests/fish.nix
+++ b/nixos/tests/fish.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "fish";
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
 
     {
diff --git a/nixos/tests/fluentd.nix b/nixos/tests/fluentd.nix
index 918f2f87db17f..150638f246f26 100644
--- a/nixos/tests/fluentd.nix
+++ b/nixos/tests/fluentd.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "fluentd";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.fluentd = {
       enable = true;
       config = ''
diff --git a/nixos/tests/fontconfig-default-fonts.nix b/nixos/tests/fontconfig-default-fonts.nix
index 58d0f6227cc7b..664afc9bf44c4 100644
--- a/nixos/tests/fontconfig-default-fonts.nix
+++ b/nixos/tests/fontconfig-default-fonts.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ lib, ... }:
     jtojnar
   ];
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     fonts.enableDefaultFonts = true; # Background fonts
     fonts.fonts = with pkgs; [
       noto-fonts-emoji
diff --git a/nixos/tests/freeswitch.nix b/nixos/tests/freeswitch.nix
index bcc6a9cb3586c..bfb7339ec3c0d 100644
--- a/nixos/tests/freeswitch.nix
+++ b/nixos/tests/freeswitch.nix
@@ -24,6 +24,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   testScript = ''
     node0.wait_for_unit("freeswitch.service")
     # Wait for SIP port to be open
-    node0.wait_for_open_port("5060")
+    node0.wait_for_open_port(5060)
   '';
 })
diff --git a/nixos/tests/fsck.nix b/nixos/tests/fsck.nix
index 5453f3bc48b59..5b8b09f433a22 100644
--- a/nixos/tests/fsck.nix
+++ b/nixos/tests/fsck.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "fsck";
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     virtualisation.emptyDiskImages = [ 1 ];
 
     virtualisation.fileSystems = {
diff --git a/nixos/tests/ft2-clone.nix b/nixos/tests/ft2-clone.nix
index 71eda43e2b245..3c90b3d3fa201 100644
--- a/nixos/tests/ft2-clone.nix
+++ b/nixos/tests/ft2-clone.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/geth.nix b/nixos/tests/geth.nix
index af8230553bbbb..11ad1ed2ea66f 100644
--- a/nixos/tests/geth.nix
+++ b/nixos/tests/geth.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = with maintainers; [bachp ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.geth."mainnet" = {
       enable = true;
       http = {
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index dc3b889c8e8e2..4f7d3f07f0657 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -1,12 +1,34 @@
-# This test runs gitlab and checks if it works
+# This test runs gitlab and performs the following tests:
+# - Creating users
+# - Pushing commits
+#   - over the API
+#   - over SSH
+# - Creating Merge Requests and merging them
+# - Opening and closing issues.
+# - Downloading repository archives as tar.gz and tar.bz2
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
 
 let
+  inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
   initialRootPassword = "notproduction";
-in
-import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
+  rootProjectId = "2";
+
+  aliceUsername = "alice";
+  aliceUserId = "2";
+  alicePassword = "alicepassword";
+  aliceProjectId = "2";
+  aliceProjectName = "test-alice";
+
+  bobUsername = "bob";
+  bobUserId = "3";
+  bobPassword = "bobpassword";
+  bobProjectId = "3";
+in {
   name = "gitlab";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ globin ];
+    maintainers = [ globin yayayayaka ];
   };
 
   nodes = {
@@ -31,6 +53,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
         };
       };
 
+      services.openssh.enable = true;
+
       services.dovecot2 = {
         enable = true;
         enableImap = true;
@@ -77,8 +101,43 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
         password = initialRootPassword;
       });
 
-      createProject = pkgs.writeText "create-project.json" (builtins.toJSON {
-        name = "test";
+      createUserAlice = pkgs.writeText "create-user-alice.json" (builtins.toJSON rec {
+        username = aliceUsername;
+        name = username;
+        email = "alice@localhost";
+        password = alicePassword;
+        skip_confirmation = true;
+      });
+
+      createUserBob = pkgs.writeText "create-user-bob.json" (builtins.toJSON rec {
+        username = bobUsername;
+        name = username;
+        email = "bob@localhost";
+        password = bobPassword;
+        skip_confirmation = true;
+      });
+
+      aliceAuth = pkgs.writeText "alice-auth.json" (builtins.toJSON {
+        grant_type = "password";
+        username = aliceUsername;
+        password = alicePassword;
+      });
+
+      bobAuth = pkgs.writeText "bob-auth.json" (builtins.toJSON {
+        grant_type = "password";
+        username = bobUsername;
+        password = bobPassword;
+      });
+
+      aliceAddSSHKey = pkgs.writeText "alice-add-ssh-key.json" (builtins.toJSON {
+        id = aliceUserId;
+        title = "snakeoil@nixos";
+        key = snakeOilPublicKey;
+      });
+
+      createProjectAlice = pkgs.writeText "create-project-alice.json" (builtins.toJSON {
+        name = aliceProjectName;
+        visibility = "public";
       });
 
       putFile = pkgs.writeText "put-file.json" (builtins.toJSON {
@@ -89,6 +148,23 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
         commit_message = "create a new file";
       });
 
+      mergeRequest = pkgs.writeText "merge-request.json" (builtins.toJSON {
+        id = bobProjectId;
+        target_project_id = aliceProjectId;
+        source_branch = "master";
+        target_branch = "master";
+        title = "Add some other file";
+      });
+
+      newIssue = pkgs.writeText "new-issue.json" (builtins.toJSON {
+        title = "useful issue title";
+      });
+
+      closeIssue = pkgs.writeText "close-issue.json" (builtins.toJSON {
+        issue_iid = 1;
+        state_event = "close";
+      });
+
       # Wait for all GitLab services to be fully started.
       waitForServices = ''
         gitlab.wait_for_unit("gitaly.service")
@@ -105,6 +181,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
       # The actual test of GitLab. Only push data to GitLab if
       # `doSetup` is is true.
       test = doSetup: ''
+        GIT_SSH_COMMAND = "ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null"
+
         gitlab.succeed(
             "curl -isSf http://gitlab | grep -i location | grep http://gitlab/users/sign_in"
         )
@@ -112,24 +190,225 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
             "${pkgs.sudo}/bin/sudo -u gitlab -H gitlab-rake gitlab:check 1>&2"
         )
         gitlab.succeed(
-            "echo \"Authorization: Bearer \$(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers"
+            "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers"
         )
       '' + optionalString doSetup ''
-        gitlab.succeed(
-            "curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createProject} http://gitlab/api/v4/projects"
-        )
-        gitlab.succeed(
-            "curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${putFile} http://gitlab/api/v4/projects/1/repository/files/some-file.txt"
-        )
+        with subtest("Create user Alice"):
+            gitlab.succeed(
+                """[ "$(curl -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createUserAlice} http://gitlab/api/v4/users)" = "201" ]"""
+            )
+            gitlab.succeed(
+                "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${aliceAuth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers-alice"
+            )
+
+        with subtest("Create user Bob"):
+            gitlab.succeed(
+                """ [ "$(curl -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createUserBob} http://gitlab/api/v4/users)" = "201" ]"""
+            )
+            gitlab.succeed(
+                "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${bobAuth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers-bob"
+            )
+
+        with subtest("Setup Git and SSH for Alice"):
+            gitlab.succeed("git config --global user.name Alice")
+            gitlab.succeed("git config --global user.email alice@nixos.invalid")
+            gitlab.succeed("mkdir -m 700 /root/.ssh")
+            gitlab.succeed("cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa")
+            gitlab.succeed("chmod 600 /root/.ssh/id_ecdsa")
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-alice -d @${aliceAddSSHKey} \
+                    http://gitlab/api/v4/user/keys)" = "201" ]
+                """
+            )
+
+        with subtest("Create a new repository"):
+            # Alice creates a new repository
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-alice \
+                    -d @${createProjectAlice} \
+                    http://gitlab/api/v4/projects)" = "201" ]
+                """
+            )
+
+            # Alice commits an initial commit
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-alice \
+                    -d @${putFile} \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/repository/files/some-file.txt)" = "201" ]"""
+            )
+
+        with subtest("git clone over HTTP"):
+            gitlab.succeed(
+                """git clone http://gitlab/alice/${aliceProjectName}.git clone-via-http""",
+                timeout=15
+            )
+
+        with subtest("Push a commit via SSH"):
+            gitlab.succeed(
+                f"""GIT_SSH_COMMAND="{GIT_SSH_COMMAND}" git clone gitlab@gitlab:alice/${aliceProjectName}.git""",
+                timeout=15
+            )
+            gitlab.succeed(
+                """echo "a commit sent over ssh" > ${aliceProjectName}/ssh.txt"""
+            )
+            gitlab.succeed(
+                """
+                cd ${aliceProjectName} || exit 1
+                git add .
+                """
+            )
+            gitlab.succeed(
+                """
+                cd ${aliceProjectName} || exit 1
+                git commit -m "Add a commit to be sent over ssh"
+                """
+            )
+            gitlab.succeed(
+                f"""
+                cd ${aliceProjectName} || exit 1
+                GIT_SSH_COMMAND="{GIT_SSH_COMMAND}" git push --set-upstream origin master
+                """,
+                timeout=15
+            )
+
+        with subtest("Fork a project"):
+            # Bob forks Alice's project
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-bob \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/fork)" = "201" ]
+                """
+            )
+
+            # Bob creates a commit
+            gitlab.wait_until_succeeds(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-bob \
+                    -d @${putFile} \
+                    http://gitlab/api/v4/projects/${bobProjectId}/repository/files/some-other-file.txt)" = "201" ]
+                """
+            )
+
+        with subtest("Create a Merge Request"):
+            # Bob opens a merge request against Alice's repository
+            gitlab.wait_until_succeeds(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-bob \
+                    -d @${mergeRequest} \
+                    http://gitlab/api/v4/projects/${bobProjectId}/merge_requests)" = "201" ]
+                """
+            )
+
+            # Alice merges the MR
+            gitlab.wait_until_succeeds(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X PUT \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-alice \
+                    -d @${mergeRequest} \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/merge_requests/1/merge)" = "200" ]
+                """
+            )
+
+        with subtest("Create an Issue"):
+            # Bob opens an issue on Alice's repository
+            gitlab.succeed(
+                """[ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X POST \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-bob \
+                    -d @${newIssue} \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/issues)" = "201" ]
+                """
+            )
+
+            # Alice closes the issue
+            gitlab.wait_until_succeeds(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -X PUT \
+                    -H 'Content-Type: application/json' \
+                    -H @/tmp/headers-alice -d @${closeIssue} http://gitlab/api/v4/projects/${aliceProjectId}/issues/1)" = "200" ]
+                """
+            )
       '' + ''
-        gitlab.succeed(
-            "curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.gz > /tmp/archive.tar.gz"
-        )
-        gitlab.succeed(
-            "curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.bz2 > /tmp/archive.tar.bz2"
-        )
-        gitlab.succeed("test -s /tmp/archive.tar.gz")
-        gitlab.succeed("test -s /tmp/archive.tar.bz2")
+        with subtest("Download archive.tar.gz"):
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -H @/tmp/headers-alice \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.gz)" = "200" ]
+                """
+            )
+            gitlab.succeed(
+                """
+                curl \
+                    -H @/tmp/headers-alice \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.gz > /tmp/archive.tar.gz
+                """
+            )
+            gitlab.succeed("test -s /tmp/archive.tar.gz")
+
+        with subtest("Download archive.tar.bz2"):
+            gitlab.succeed(
+                """
+                [ "$(curl \
+                    -o /dev/null \
+                    -w '%{http_code}' \
+                    -H @/tmp/headers-alice \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.bz2)" = "200" ]
+                """
+            )
+            gitlab.succeed(
+                """
+                curl \
+                    -H @/tmp/headers-alice \
+                    http://gitlab/api/v4/projects/${aliceProjectId}/repository/archive.tar.bz2 > /tmp/archive.tar.bz2
+                """
+            )
+            gitlab.succeed("test -s /tmp/archive.tar.bz2")
       '';
 
   in ''
diff --git a/nixos/tests/gitolite-fcgiwrap.nix b/nixos/tests/gitolite-fcgiwrap.nix
index fc9b214b762ee..abf1db37003a6 100644
--- a/nixos/tests/gitolite-fcgiwrap.nix
+++ b/nixos/tests/gitolite-fcgiwrap.nix
@@ -20,7 +20,7 @@ import ./make-test-python.nix (
         nodes = {
 
           server =
-            { ... }:
+            { config, ... }:
               {
                 networking.firewall.allowedTCPPorts = [ 80 ];
 
@@ -42,7 +42,7 @@ import ./make-test-python.nix (
                     auth_basic_user_file /etc/gitolite/htpasswd;
 
                     # common FastCGI parameters are required
-                    include ${pkgs.nginx}/conf/fastcgi_params;
+                    include ${config.services.nginx.package}/conf/fastcgi_params;
 
                     # strip the CGI program prefix
                     fastcgi_split_path_info ^(/git)(.*)$;
diff --git a/nixos/tests/gitolite.nix b/nixos/tests/gitolite.nix
index 128677cebde3a..9b3af59e4fbd8 100644
--- a/nixos/tests/gitolite.nix
+++ b/nixos/tests/gitolite.nix
@@ -107,7 +107,7 @@ in
     with subtest("gitolite server starts"):
         server.wait_for_unit("gitolite-init.service")
         server.wait_for_unit("sshd.service")
-        client.succeed("ssh gitolite@server info")
+        client.succeed("ssh -n gitolite@server info")
 
     with subtest("admin can clone and configure gitolite-admin.git"):
         client.succeed(
diff --git a/nixos/tests/gnome-xorg.nix b/nixos/tests/gnome-xorg.nix
index 6264b87af4ec5..618458b1f6b5b 100644
--- a/nixos/tests/gnome-xorg.nix
+++ b/nixos/tests/gnome-xorg.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     maintainers = teams.gnome.members;
   };
 
-  machine = { nodes, ... }: let
+  nodes.machine = { nodes, ... }: let
     user = nodes.machine.config.users.users.alice;
   in
 
@@ -24,6 +24,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       services.xserver.desktopManager.gnome.enable = true;
       services.xserver.desktopManager.gnome.debug = true;
       services.xserver.displayManager.defaultSession = "gnome-xorg";
+      programs.gnome-terminal.enable = true;
 
       systemd.user.services = {
         "org.gnome.Shell@x11" = {
diff --git a/nixos/tests/gnome.nix b/nixos/tests/gnome.nix
index 06f387ecad67d..05619cbd7d82a 100644
--- a/nixos/tests/gnome.nix
+++ b/nixos/tests/gnome.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     maintainers = teams.gnome.members;
   };
 
-  machine =
+  nodes.machine =
     { ... }:
 
     { imports = [ ./common/user-account.nix ];
@@ -22,6 +22,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
       services.xserver.desktopManager.gnome.enable = true;
       services.xserver.desktopManager.gnome.debug = true;
+      programs.gnome-terminal.enable = true;
 
       environment.systemPackages = [
         (pkgs.makeAutostartItem {
diff --git a/nixos/tests/gocd-agent.nix b/nixos/tests/gocd-agent.nix
index 686d0b971d30b..9301a88ec05d8 100644
--- a/nixos/tests/gocd-agent.nix
+++ b/nixos/tests/gocd-agent.nix
@@ -36,7 +36,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   testScript = ''
     start_all()
     agent.wait_for_unit("gocd-server")
-    agent.wait_for_open_port("8153")
+    agent.wait_for_open_port(8153)
     agent.wait_for_unit("gocd-agent")
     agent.wait_until_succeeds(
         "curl ${serverUrl} -H '${header}' | ${pkgs.jq}/bin/jq -e ._embedded.agents[0].uuid"
diff --git a/nixos/tests/gotify-server.nix b/nixos/tests/gotify-server.nix
index 051666fbe72e7..e7942b76d8e50 100644
--- a/nixos/tests/gotify-server.nix
+++ b/nixos/tests/gotify-server.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     maintainers = [ ma27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.jq ];
 
     services.gotify = {
diff --git a/nixos/tests/grafana-agent.nix b/nixos/tests/grafana-agent.nix
new file mode 100644
index 0000000000000..a9f34d8cea31a
--- /dev/null
+++ b/nixos/tests/grafana-agent.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+  let
+    nodes = {
+      machine = {
+        services.grafana-agent = {
+          enable = true;
+        };
+      };
+    };
+  in
+  {
+    name = "grafana-agent";
+
+    meta = with lib.maintainers; {
+      maintainers = [ zimbatm ];
+    };
+
+    inherit nodes;
+
+    testScript = ''
+      start_all()
+
+      with subtest("Grafana-agent is running"):
+          machine.wait_for_unit("grafana-agent.service")
+          machine.wait_for_open_port(12345)
+          machine.succeed(
+              "curl -sSfN http://127.0.0.1:12345/-/healthy"
+          )
+          machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/graphite.nix b/nixos/tests/graphite.nix
index 496f16846ea6a..c534d45428e19 100644
--- a/nixos/tests/graphite.nix
+++ b/nixos/tests/graphite.nix
@@ -12,14 +12,8 @@ import ./make-test-python.nix ({ pkgs, ... } :
               SECRET_KEY = "abcd";
             '';
           };
-          api = {
-            enable = true;
-            port = 8082;
-            finders = [ ];
-          };
           carbon.enableCache = true;
           seyren.enable = false;  # Implicitely requires openssl-1.0.2u which is marked insecure
-          beacon.enable = true;
         };
       };
   };
@@ -28,21 +22,15 @@ import ./make-test-python.nix ({ pkgs, ... } :
     start_all()
     one.wait_for_unit("default.target")
     one.wait_for_unit("graphiteWeb.service")
-    one.wait_for_unit("graphiteApi.service")
-    one.wait_for_unit("graphite-beacon.service")
     one.wait_for_unit("carbonCache.service")
     # The services above are of type "simple". systemd considers them active immediately
     # even if they're still in preStart (which takes quite long for graphiteWeb).
     # Wait for ports to open so we're sure the services are up and listening.
     one.wait_for_open_port(8080)
-    one.wait_for_open_port(8082)
     one.wait_for_open_port(2003)
     one.succeed('echo "foo 1 `date +%s`" | nc -N localhost 2003')
     one.wait_until_succeeds(
         "curl 'http://localhost:8080/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
     )
-    one.wait_until_succeeds(
-        "curl 'http://localhost:8082/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
-    )
   '';
 })
diff --git a/nixos/tests/graylog.nix b/nixos/tests/graylog.nix
index 572904f60d575..23f426fc7af95 100644
--- a/nixos/tests/graylog.nix
+++ b/nixos/tests/graylog.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "graylog";
   meta.maintainers = with lib.maintainers; [ ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.memorySize = 4096;
     virtualisation.diskSize = 4096;
 
diff --git a/nixos/tests/grocy.nix b/nixos/tests/grocy.nix
index 2be5c24ecb55c..fe0ddd341486b 100644
--- a/nixos/tests/grocy.nix
+++ b/nixos/tests/grocy.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ ma27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.grocy = {
       enable = true;
       hostName = "localhost";
diff --git a/nixos/tests/grub.nix b/nixos/tests/grub.nix
index 84bfc90955b54..e0875e70f6a51 100644
--- a/nixos/tests/grub.nix
+++ b/nixos/tests/grub.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ lib, ... }: {
     maintainers = [ rnhmjoj ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     virtualisation.useBootLoader = true;
 
     boot.loader.timeout = null;
diff --git a/nixos/tests/hadoop/default.nix b/nixos/tests/hadoop/default.nix
new file mode 100644
index 0000000000000..d2a97cbeffb8d
--- /dev/null
+++ b/nixos/tests/hadoop/default.nix
@@ -0,0 +1,7 @@
+{ handleTestOn, package, ... }:
+
+{
+  all = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./hadoop.nix { inherit package; };
+  hdfs = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./hdfs.nix { inherit package; };
+  yarn = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./yarn.nix { inherit package; };
+}
diff --git a/nixos/tests/hadoop/hadoop.nix b/nixos/tests/hadoop/hadoop.nix
index 48737debab546..b132f4fa58b01 100644
--- a/nixos/tests/hadoop/hadoop.nix
+++ b/nixos/tests/hadoop/hadoop.nix
@@ -1,121 +1,148 @@
 # This test is very comprehensive. It tests whether all hadoop services work well with each other.
 # Run this when updating the Hadoop package or making significant changes to the hadoop module.
 # For a more basic test, see hdfs.nix and yarn.nix
-import ../make-test-python.nix ({pkgs, ...}: {
-
-  nodes = let
-    package = pkgs.hadoop;
-    coreSite = {
-      "fs.defaultFS" = "hdfs://ns1";
-    };
-    hdfsSite = {
-      "dfs.namenode.rpc-bind-host" = "0.0.0.0";
-      "dfs.namenode.http-bind-host" = "0.0.0.0";
-      "dfs.namenode.servicerpc-bind-host" = "0.0.0.0";
-
-      # HA Quorum Journal Manager configuration
-      "dfs.nameservices" = "ns1";
-      "dfs.ha.namenodes.ns1" = "nn1,nn2";
-      "dfs.namenode.shared.edits.dir.ns1.nn1" = "qjournal://jn1:8485;jn2:8485;jn3:8485/ns1";
-      "dfs.namenode.shared.edits.dir.ns1.nn2" = "qjournal://jn1:8485;jn2:8485;jn3:8485/ns1";
-      "dfs.namenode.rpc-address.ns1.nn1" = "nn1:8020";
-      "dfs.namenode.rpc-address.ns1.nn2" = "nn2:8020";
-      "dfs.namenode.servicerpc-address.ns1.nn1" = "nn1:8022";
-      "dfs.namenode.servicerpc-address.ns1.nn2" = "nn2:8022";
-      "dfs.namenode.http-address.ns1.nn1" = "nn1:9870";
-      "dfs.namenode.http-address.ns1.nn2" = "nn2:9870";
-
-      # Automatic failover configuration
-      "dfs.client.failover.proxy.provider.ns1" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider";
-      "dfs.ha.automatic-failover.enabled.ns1" = "true";
-      "dfs.ha.fencing.methods" = "shell(true)";
-      "ha.zookeeper.quorum" = "zk1:2181";
-    };
-    yarnSiteHA = {
-      "yarn.resourcemanager.zk-address" = "zk1:2181";
-      "yarn.resourcemanager.ha.enabled" = "true";
-      "yarn.resourcemanager.ha.rm-ids" = "rm1,rm2";
-      "yarn.resourcemanager.hostname.rm1" = "rm1";
-      "yarn.resourcemanager.hostname.rm2" = "rm2";
-      "yarn.resourcemanager.ha.automatic-failover.enabled" = "true";
-      "yarn.resourcemanager.cluster-id" = "cluster1";
-      # yarn.resourcemanager.webapp.address needs to be defined even though yarn.resourcemanager.hostname is set. This shouldn't be necessary, but there's a bug in
-      # hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmFilterInitializer.java:70
-      # that causes AM containers to fail otherwise.
-      "yarn.resourcemanager.webapp.address.rm1" = "rm1:8088";
-      "yarn.resourcemanager.webapp.address.rm2" = "rm2:8088";
-    };
-  in {
-    zk1 = { ... }: {
-      services.zookeeper.enable = true;
-      networking.firewall.allowedTCPPorts = [ 2181 ];
-    };
-
-    # HDFS cluster
-    nn1 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.namenode.enable = true;
-        hdfs.zkfc.enable = true;
+import ../make-test-python.nix ({ package, ... }: {
+  name = "hadoop-combined";
+
+  nodes =
+    let
+      coreSite = {
+        "fs.defaultFS" = "hdfs://ns1";
+      };
+      hdfsSite = {
+        # HA Quorum Journal Manager configuration
+        "dfs.nameservices" = "ns1";
+        "dfs.ha.namenodes.ns1" = "nn1,nn2";
+        "dfs.namenode.shared.edits.dir.ns1" = "qjournal://jn1:8485;jn2:8485;jn3:8485/ns1";
+        "dfs.namenode.rpc-address.ns1.nn1" = "nn1:8020";
+        "dfs.namenode.rpc-address.ns1.nn2" = "nn2:8020";
+        "dfs.namenode.servicerpc-address.ns1.nn1" = "nn1:8022";
+        "dfs.namenode.servicerpc-address.ns1.nn2" = "nn2:8022";
+        "dfs.namenode.http-address.ns1.nn1" = "nn1:9870";
+        "dfs.namenode.http-address.ns1.nn2" = "nn2:9870";
+
+        # Automatic failover configuration
+        "dfs.client.failover.proxy.provider.ns1" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider";
+        "dfs.ha.automatic-failover.enabled.ns1" = "true";
+        "dfs.ha.fencing.methods" = "shell(true)";
+        "ha.zookeeper.quorum" = "zk1:2181";
+      };
+      yarnSite = {
+        "yarn.resourcemanager.zk-address" = "zk1:2181";
+        "yarn.resourcemanager.ha.enabled" = "true";
+        "yarn.resourcemanager.ha.rm-ids" = "rm1,rm2";
+        "yarn.resourcemanager.hostname.rm1" = "rm1";
+        "yarn.resourcemanager.hostname.rm2" = "rm2";
+        "yarn.resourcemanager.ha.automatic-failover.enabled" = "true";
+        "yarn.resourcemanager.cluster-id" = "cluster1";
+        # yarn.resourcemanager.webapp.address needs to be defined even though yarn.resourcemanager.hostname is set. This shouldn't be necessary, but there's a bug in
+        # hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmFilterInitializer.java:70
+        # that causes AM containers to fail otherwise.
+        "yarn.resourcemanager.webapp.address.rm1" = "rm1:8088";
+        "yarn.resourcemanager.webapp.address.rm2" = "rm2:8088";
+      };
+    in
+    {
+      zk1 = { ... }: {
+        services.zookeeper.enable = true;
+        networking.firewall.allowedTCPPorts = [ 2181 ];
       };
-    };
-    nn2 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.namenode.enable = true;
-        hdfs.zkfc.enable = true;
+
+      # HDFS cluster
+      nn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.namenode = {
+            enable = true;
+            openFirewall = true;
+          };
+          hdfs.zkfc.enable = true;
+        };
+      };
+      nn2 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.namenode = {
+            enable = true;
+            openFirewall = true;
+          };
+          hdfs.zkfc.enable = true;
+        };
       };
-    };
 
-    jn1 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.journalnode.enable = true;
+      jn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
-    jn2 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.journalnode.enable = true;
+      jn2 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
-    jn3 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.journalnode.enable = true;
+      jn3 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+
+      dn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.datanode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
 
-    dn1 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        hdfs.datanode.enable = true;
+      # YARN cluster
+      rm1 = { options, ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.resourcemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
-
-    # YARN cluster
-    rm1 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        yarnSite = options.services.hadoop.yarnSite.default // yarnSiteHA;
-        yarn.resourcemanager.enable = true;
+      rm2 = { options, ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.resourcemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
-    rm2 = {pkgs, options, ...}: {
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        yarnSite = options.services.hadoop.yarnSite.default // yarnSiteHA;
-        yarn.resourcemanager.enable = true;
+      nm1 = { options, ... }: {
+        virtualisation.memorySize = 2048;
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.nodemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
       };
-    };
-    nm1 = {pkgs, options, ...}: {
-      virtualisation.memorySize = 2048;
-      services.hadoop = {
-        inherit package coreSite hdfsSite;
-        yarnSite = options.services.hadoop.yarnSite.default // yarnSiteHA;
-        yarn.nodemanager.enable = true;
+      client = { options, ... }: {
+        services.hadoop = {
+          gatewayRole.enable = true;
+          inherit package coreSite hdfsSite yarnSite;
+        };
       };
-    };
   };
 
   testScript = ''
@@ -173,26 +200,26 @@ import ../make-test-python.nix ({pkgs, ...}: {
     # DN should have started by now, but confirm anyway
     dn1.wait_for_unit("hdfs-datanode")
     # Print states of namenodes
-    dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
     # Wait for cluster to exit safemode
-    dn1.succeed("sudo -u hdfs hdfs dfsadmin -safemode wait")
-    dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    client.succeed("sudo -u hdfs hdfs dfsadmin -safemode wait")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
     # test R/W
-    dn1.succeed("echo testfilecontents | sudo -u hdfs hdfs dfs -put - /testfile")
-    assert "testfilecontents" in dn1.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
+    client.succeed("echo testfilecontents | sudo -u hdfs hdfs dfs -put - /testfile")
+    assert "testfilecontents" in client.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
 
     # Test NN failover
     nn1.succeed("systemctl stop hdfs-namenode")
-    assert "active" in dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
-    dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
-    assert "testfilecontents" in dn1.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
+    assert "active" in client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    assert "testfilecontents" in client.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
 
     nn1.succeed("systemctl start hdfs-namenode")
     nn1.wait_for_open_port(9870)
     nn1.wait_for_open_port(8022)
     nn1.wait_for_open_port(8020)
-    assert "standby" in dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
-    dn1.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    assert "standby" in client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
 
     #### YARN tests ####
 
@@ -208,21 +235,21 @@ import ../make-test-python.nix ({pkgs, ...}: {
     nm1.wait_for_unit("yarn-nodemanager")
     nm1.wait_for_open_port(8042)
     nm1.wait_for_open_port(8040)
-    nm1.wait_until_succeeds("yarn node -list | grep Nodes:1")
-    nm1.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
-    nm1.succeed("sudo -u yarn yarn node -list | systemd-cat")
+    client.wait_until_succeeds("yarn node -list | grep Nodes:1")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+    client.succeed("sudo -u yarn yarn node -list | systemd-cat")
 
     # Test RM failover
     rm1.succeed("systemctl stop yarn-resourcemanager")
-    assert "standby" not in nm1.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
-    nm1.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+    assert "standby" not in client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
     rm1.succeed("systemctl start yarn-resourcemanager")
     rm1.wait_for_unit("yarn-resourcemanager")
     rm1.wait_for_open_port(8088)
-    assert "standby" in nm1.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
-    nm1.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+    assert "standby" in client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
 
-    assert "Estimated value of Pi is" in nm1.succeed("HADOOP_USER_NAME=hdfs yarn jar $(readlink $(which yarn) | sed -r 's~bin/yarn~lib/hadoop-*/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar~g') pi 2 10")
-    assert "SUCCEEDED" in nm1.succeed("yarn application -list -appStates FINISHED")
+    assert "Estimated value of Pi is" in client.succeed("HADOOP_USER_NAME=hdfs yarn jar $(readlink $(which yarn) | sed -r 's~bin/yarn~lib/hadoop-*/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar~g') pi 2 10")
+    assert "SUCCEEDED" in client.succeed("yarn application -list -appStates FINISHED")
   '';
 })
diff --git a/nixos/tests/hadoop/hdfs.nix b/nixos/tests/hadoop/hdfs.nix
index b63cbf4803271..9415500463dec 100644
--- a/nixos/tests/hadoop/hdfs.nix
+++ b/nixos/tests/hadoop/hdfs.nix
@@ -1,32 +1,46 @@
 # Test a minimal HDFS cluster with no HA
-import ../make-test-python.nix ({...}: {
-  nodes = {
-    namenode = {pkgs, ...}: {
+import ../make-test-python.nix ({ package, lib, ... }:
+with lib;
+{
+  name = "hadoop-hdfs";
+
+  nodes = let
+    coreSite = {
+      "fs.defaultFS" = "hdfs://namenode:8020";
+      "hadoop.proxyuser.httpfs.groups" = "*";
+      "hadoop.proxyuser.httpfs.hosts" = "*";
+    };
+    in {
+    namenode = { pkgs, ... }: {
       services.hadoop = {
-        package = pkgs.hadoop;
+        inherit package;
         hdfs = {
           namenode = {
             enable = true;
+            openFirewall = true;
             formatOnInit = true;
           };
-          httpfs.enable = true;
-        };
-        coreSite = {
-          "fs.defaultFS" = "hdfs://namenode:8020";
-          "hadoop.proxyuser.httpfs.groups" = "*";
-          "hadoop.proxyuser.httpfs.hosts" = "*";
+          httpfs = {
+            # The NixOS hadoop module only support webHDFS on 3.3 and newer
+            enable = mkIf (versionAtLeast package.version "3.3") true;
+            openFirewall = true;
+          };
         };
+        inherit coreSite;
       };
     };
-    datanode = {pkgs, ...}: {
+    datanode = { pkgs, ... }: {
       services.hadoop = {
-        package = pkgs.hadoop;
-        hdfs.datanode.enable = true;
-        coreSite = {
-          "fs.defaultFS" = "hdfs://namenode:8020";
-          "hadoop.proxyuser.httpfs.groups" = "*";
-          "hadoop.proxyuser.httpfs.hosts" = "*";
+        inherit package;
+        hdfs.datanode = {
+          enable = true;
+          openFirewall = true;
+          dataDirs = [{
+            type = "DISK";
+            path = "/tmp/dn1";
+          }];
         };
+        inherit coreSite;
       };
     };
   };
@@ -37,21 +51,32 @@ import ../make-test-python.nix ({...}: {
     namenode.wait_for_unit("hdfs-namenode")
     namenode.wait_for_unit("network.target")
     namenode.wait_for_open_port(8020)
+    namenode.succeed("ss -tulpne | systemd-cat")
+    namenode.succeed("cat /etc/hadoop*/hdfs-site.xml | systemd-cat")
     namenode.wait_for_open_port(9870)
 
     datanode.wait_for_unit("hdfs-datanode")
     datanode.wait_for_unit("network.target")
+  '' + ( if versionAtLeast package.version "3" then ''
     datanode.wait_for_open_port(9864)
     datanode.wait_for_open_port(9866)
     datanode.wait_for_open_port(9867)
 
-    namenode.succeed("curl -f http://namenode:9870")
     datanode.succeed("curl -f http://datanode:9864")
+  '' else ''
+    datanode.wait_for_open_port(50075)
+    datanode.wait_for_open_port(50010)
+    datanode.wait_for_open_port(50020)
+
+    datanode.succeed("curl -f http://datanode:50075")
+  '' ) + ''
+    namenode.succeed("curl -f http://namenode:9870")
 
     datanode.succeed("sudo -u hdfs hdfs dfsadmin -safemode wait")
     datanode.succeed("echo testfilecontents | sudo -u hdfs hdfs dfs -put - /testfile")
     assert "testfilecontents" in datanode.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
 
+  '' + optionalString ( versionAtLeast package.version "3.3" ) ''
     namenode.wait_for_unit("hdfs-httpfs")
     namenode.wait_for_open_port(14000)
     assert "testfilecontents" in datanode.succeed("curl -f \"http://namenode:14000/webhdfs/v1/testfile?user.name=hdfs&op=OPEN\" 2>&1")
diff --git a/nixos/tests/hadoop/yarn.nix b/nixos/tests/hadoop/yarn.nix
index 09bdb35791c7e..1bf8e3831f67a 100644
--- a/nixos/tests/hadoop/yarn.nix
+++ b/nixos/tests/hadoop/yarn.nix
@@ -1,22 +1,30 @@
 # This only tests if YARN is able to start its services
-import ../make-test-python.nix ({...}: {
+import ../make-test-python.nix ({ package, ... }: {
+  name = "hadoop-yarn";
+
   nodes = {
-    resourcemanager = {pkgs, ...}: {
-      services.hadoop.package = pkgs.hadoop;
-      services.hadoop.yarn.resourcemanager.enable = true;
-      services.hadoop.yarnSite = {
-        "yarn.resourcemanager.scheduler.class" = "org.apache.hadoop.yarn.server.resourcemanager.scheduler.fifo.FifoScheduler";
+    resourcemanager = { ... }: {
+      services.hadoop = {
+        inherit package;
+        yarn.resourcemanager = {
+          enable = true;
+          openFirewall = true;
+        };
       };
     };
-    nodemanager = {pkgs, ...}: {
-      services.hadoop.package = pkgs.hadoop;
-      services.hadoop.yarn.nodemanager.enable = true;
-      services.hadoop.yarnSite = {
-        "yarn.resourcemanager.hostname" = "resourcemanager";
-        "yarn.nodemanager.log-dirs" = "/tmp/userlogs";
+    nodemanager = { options, lib, ... }: {
+      services.hadoop = {
+        inherit package;
+        yarn.nodemanager = {
+          enable = true;
+          openFirewall = true;
+        };
+        yarnSite = options.services.hadoop.yarnSite.default // {
+          "yarn.resourcemanager.hostname" = "resourcemanager";
+          "yarn.nodemanager.log-dirs" = "/tmp/userlogs";
+        };
       };
     };
-
   };
 
   testScript = ''
diff --git a/nixos/tests/hardened.nix b/nixos/tests/hardened.nix
index dc455f971f5c1..ccb8581685478 100644
--- a/nixos/tests/hardened.nix
+++ b/nixos/tests/hardened.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     maintainers = [ joachifm ];
   };
 
-  machine =
+  nodes.machine =
     { lib, pkgs, config, ... }:
     with lib;
     { users.users.alice = { isNormalUser = true; extraGroups = [ "proc" ]; };
@@ -12,6 +12,11 @@ import ./make-test-python.nix ({ pkgs, ... } : {
       imports = [ ../modules/profiles/hardened.nix ];
       environment.memoryAllocator.provider = "graphene-hardened";
       nix.settings.sandbox = false;
+      nixpkgs.overlays = [
+        (final: super: {
+          dhcpcd = super.dhcpcd.override { enablePrivSep = false; };
+        })
+      ];
       virtualisation.emptyDiskImages = [ 4096 ];
       boot.initrd.postDeviceCommands = ''
         ${pkgs.dosfstools}/bin/mkfs.vfat -n EFISYS /dev/vdb
@@ -85,8 +90,8 @@ import ./make-test-python.nix ({ pkgs, ... } : {
 
       # Test Nix dæmon usage
       with subtest("nix-daemon cannot be used by all users"):
-          machine.fail("su -l nobody -s /bin/sh -c 'nix ping-store'")
-          machine.succeed("su -l alice -c 'nix ping-store'")
+          machine.fail("su -l nobody -s /bin/sh -c 'nix --extra-experimental-features nix-command ping-store'")
+          machine.succeed("su -l alice -c 'nix --extra-experimental-features nix-command ping-store'")
 
 
       # Test kernel image protection
diff --git a/nixos/tests/haste-server.nix b/nixos/tests/haste-server.nix
new file mode 100644
index 0000000000000..9097c992c5483
--- /dev/null
+++ b/nixos/tests/haste-server.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  {
+    name = "haste-server";
+    meta.maintainers = with lib.maintainers; [ mkg20001 ];
+
+    nodes.machine = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [
+        curl
+        jq
+      ];
+
+      services.haste-server = {
+        enable = true;
+      };
+    };
+
+    testScript = ''
+      machine.wait_for_unit("haste-server")
+      machine.wait_until_succeeds("curl -s localhost:7777")
+      machine.succeed('curl -s -X POST http://localhost:7777/documents -d "Hello World!" > bla')
+      machine.succeed('curl http://localhost:7777/raw/$(cat bla | jq -r .key) | grep "Hello World"')
+    '';
+  })
diff --git a/nixos/tests/hbase.nix b/nixos/tests/hbase.nix
new file mode 100644
index 0000000000000..a449d24dd6fd8
--- /dev/null
+++ b/nixos/tests/hbase.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, lib, package ? pkgs.hbase, ... }:
+{
+  name = "hbase";
+
+  meta = with lib.maintainers; {
+    maintainers = [ illustris ];
+  };
+
+  nodes = {
+    hbase = { pkgs, ... }: {
+      services.hbase = {
+        enable = true;
+        inherit package;
+        # Needed for standalone mode in hbase 2+
+        # This setting and standalone mode are not suitable for production
+        settings."hbase.unsafe.stream.capability.enforce" = "false";
+      };
+      environment.systemPackages = with pkgs; [
+        package
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    hbase.wait_for_unit("hbase.service")
+    hbase.wait_until_succeeds("echo \"create 't1','f1'\" | sudo -u hbase hbase shell -n")
+    assert "NAME => 'f1'" in hbase.succeed("echo \"describe 't1'\" | sudo -u hbase hbase shell -n")
+  '';
+})
diff --git a/nixos/tests/hedgedoc.nix b/nixos/tests/hedgedoc.nix
index 657d49c555e99..410350d83627c 100644
--- a/nixos/tests/hedgedoc.nix
+++ b/nixos/tests/hedgedoc.nix
@@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       services = {
         hedgedoc = {
           enable = true;
-          configuration.dbURL = "sqlite:///var/lib/hedgedoc/hedgedoc.db";
+          settings.dbURL = "sqlite:///var/lib/hedgedoc/hedgedoc.db";
         };
       };
     };
@@ -21,7 +21,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       services = {
         hedgedoc = {
           enable = true;
-          configuration.dbURL = "postgres://hedgedoc:\${DB_PASSWORD}@localhost:5432/hedgedocdb";
+          settings.dbURL = "postgres://hedgedoc:\${DB_PASSWORD}@localhost:5432/hedgedocdb";
 
           /*
            * Do not use pkgs.writeText for secrets as
diff --git a/nixos/tests/herbstluftwm.nix b/nixos/tests/herbstluftwm.nix
index 7d079f4bfb695..b6965914360e7 100644
--- a/nixos/tests/herbstluftwm.nix
+++ b/nixos/tests/herbstluftwm.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ lib, ...} : {
     maintainers = with lib.maintainers; [ thibautmarty ];
   };
 
-  machine = { pkgs, lib, ... }: {
+  nodes.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";
diff --git a/nixos/tests/hibernate.nix b/nixos/tests/hibernate.nix
index 3880f1649bd3d..7a4b331169a39 100644
--- a/nixos/tests/hibernate.nix
+++ b/nixos/tests/hibernate.nix
@@ -3,6 +3,7 @@
 { system ? builtins.currentSystem
 , config ? {}
 , pkgs ? import ../.. { inherit system config; }
+, systemdStage1 ? false
 }:
 
 with import ../lib/testing-python.nix { inherit system pkgs; };
@@ -29,6 +30,11 @@ let
       "/".device = "/dev/vda2";
     };
     swapDevices = mkOverride 0 [ { device = "/dev/vda1"; } ];
+    boot.resumeDevice = mkIf systemdStage1 "/dev/vda1";
+    boot.initrd.systemd = mkIf systemdStage1 {
+      enable = true;
+      emergencyAccess = true;
+    };
   };
   installedSystem = (import ../lib/eval-config.nix {
     inherit system;
@@ -117,6 +123,11 @@ in makeTest {
       resume = create_named_machine("resume")
       resume.start()
       resume.succeed("grep 'not persisted to disk' /run/test/suspended")
+
+      # Ensure we don't restore from hibernation when booting again
+      resume.crash()
+      resume.wait_for_unit("default.target")
+      resume.fail("grep 'not persisted to disk' /run/test/suspended")
     '';
 
 }
diff --git a/nixos/tests/hitch/default.nix b/nixos/tests/hitch/default.nix
index a1d8e6162606d..4283b9f7dffbe 100644
--- a/nixos/tests/hitch/default.nix
+++ b/nixos/tests/hitch/default.nix
@@ -4,7 +4,7 @@ import ../make-test-python.nix ({ pkgs, ... }:
   meta = with pkgs.lib.maintainers; {
     maintainers = [ jflanglois ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.curl ];
     services.hitch = {
       enable = true;
diff --git a/nixos/tests/hocker-fetchdocker/default.nix b/nixos/tests/hocker-fetchdocker/default.nix
index e3979db3c60b5..b5c06126c2e80 100644
--- a/nixos/tests/hocker-fetchdocker/default.nix
+++ b/nixos/tests/hocker-fetchdocker/default.nix
@@ -5,7 +5,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     broken = true; # tries to download from registry-1.docker.io - how did this ever work?
   };
 
-  machine = import ./machine.nix;
+  nodes.machine = import ./machine.nix;
 
   testScript = ''
     start_all()
diff --git a/nixos/tests/hockeypuck.nix b/nixos/tests/hockeypuck.nix
index 19df9dee3d315..d1ef4cbf588a8 100644
--- a/nixos/tests/hockeypuck.nix
+++ b/nixos/tests/hockeypuck.nix
@@ -24,7 +24,7 @@ in {
   name = "hockeypuck";
   meta.maintainers = with lib.maintainers; [ etu ];
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     # Used for test
     environment.systemPackages = [ pkgs.gnupg ];
 
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 5b1c07c92da35..0bbeffd18cf0d 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -2,33 +2,46 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
 let
   configDir = "/var/lib/foobar";
-  mqttUsername = "homeassistant";
-  mqttPassword = "secret";
 in {
   name = "home-assistant";
   meta.maintainers = lib.teams.home-assistant.members;
 
   nodes.hass = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ mosquitto ];
-    services.mosquitto = {
+    services.postgresql = {
       enable = true;
-      listeners = [ {
-        users = {
-          "${mqttUsername}" = {
-            acl = [ "readwrite #" ];
-            password = mqttPassword;
-          };
+      ensureDatabases = [ "hass" ];
+      ensureUsers = [{
+        name = "hass";
+        ensurePermissions = {
+          "DATABASE hass" = "ALL PRIVILEGES";
         };
-      } ];
+      }];
     };
+
     services.home-assistant = {
-      inherit configDir;
       enable = true;
+      inherit configDir;
+
+      # tests loading components by overriding the package
       package = (pkgs.home-assistant.override {
+        extraPackages = ps: with ps; [
+          colorama
+        ];
         extraComponents = [ "zha" ];
       }).overrideAttrs (oldAttrs: {
         doInstallCheck = false;
       });
+
+      # tests loading components from the module
+      extraComponents = [
+        "wake_on_lan"
+      ];
+
+      # test extra package passing from the module
+      extraPackages = python3Packages: with python3Packages; [
+        psycopg2
+      ];
+
       config = {
         homeassistant = {
           name = "Home";
@@ -37,34 +50,39 @@ in {
           longitude = "0.0";
           elevation = 0;
         };
+
+        # configure the recorder component to use the postgresql db
+        recorder.db_url = "postgresql://@/hass";
+
+        # we can't load default_config, because the updater requires
+        # network access and would cause an error, so load frontend
+        # here explicitly.
+        # https://www.home-assistant.io/integrations/frontend/
         frontend = {};
-        mqtt = {
-          broker = "127.0.0.1";
-          username = mqttUsername;
-          password = mqttPassword;
-        };
-        binary_sensor = [{
-          platform = "mqtt";
-          state_topic = "home-assistant/test";
-          payload_on = "let_there_be_light";
-          payload_off = "off";
-        }];
-        wake_on_lan = {};
-        switch = [{
+
+        # set up a wake-on-lan switch to test capset capability required
+        # for the ping suid wrapper
+        # https://www.home-assistant.io/integrations/wake_on_lan/
+        switch = [ {
           platform = "wake_on_lan";
           mac = "00:11:22:33:44:55";
           host = "127.0.0.1";
-        }];
-        # tests component-based capability assignment (CAP_NET_BIND_SERVICE)
+        } ];
+
+        # test component-based capability assignment (CAP_NET_BIND_SERVICE)
+        # https://www.home-assistant.io/integrations/emulated_hue/
         emulated_hue = {
           host_ip = "127.0.0.1";
           listen_port = 80;
         };
+
+        # https://www.home-assistant.io/integrations/logger/
         logger = {
           default = "info";
-          logs."homeassistant.components.mqtt" = "debug";
         };
       };
+
+      # configure the sample lovelace dashboard
       lovelaceConfig = {
         title = "My Awesome Home";
         views = [{
@@ -78,46 +96,101 @@ in {
       };
       lovelaceConfigWritable = true;
     };
+
+    # Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
+    specialisation.differentName = {
+      inheritParentConfig = true;
+      configuration.services.home-assistant.config.homeassistant.name = lib.mkForce "Test Home";
+    };
+
+    # Cause a configuration change that requires a service restart as we added a new runtime dependency
+    specialisation.newFeature = {
+      inheritParentConfig = true;
+      configuration.services.home-assistant.config.esphome = {};
+    };
   };
 
-  testScript = ''
+  testScript = { nodes, ... }: let
+    system = nodes.hass.config.system.build.toplevel;
+  in
+  ''
+    import re
+    import json
+
     start_all()
+
+    # Parse the package path out of the systemd unit, as we cannot
+    # access the final package, that is overriden inside the module,
+    # by any other means.
+    pattern = re.compile(r"path=(?P<path>[\/a-z0-9-.]+)\/bin\/hass")
+    response = hass.execute("systemctl show -p ExecStart home-assistant.service")[1]
+    match = pattern.search(response)
+    assert match
+    package = match.group('path')
+
+
+    def get_journal_cursor(host) -> str:
+        exit, out = host.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
+        assert exit == 0
+        return json.loads(out)["__CURSOR"]
+
+
+    def wait_for_homeassistant(host, cursor):
+        host.wait_until_succeeds(f"journalctl --after-cursor='{cursor}' -u home-assistant.service | grep -q 'Home Assistant initialized in'")
+
+
     hass.wait_for_unit("home-assistant.service")
+    cursor = get_journal_cursor(hass)
+
     with subtest("Check that YAML configuration file is in place"):
         hass.succeed("test -L ${configDir}/configuration.yaml")
-    with subtest("lovelace config is copied because lovelaceConfigWritable = true"):
+
+    with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
         hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
+
+    with subtest("Check extraComponents and extraPackages are considered from the package"):
+        hass.succeed(f"grep -q 'colorama' {package}/extra_packages")
+        hass.succeed(f"grep -q 'zha' {package}/extra_components")
+
+    with subtest("Check extraComponents and extraPackages are considered from the module"):
+        hass.succeed(f"grep -q 'psycopg2' {package}/extra_packages")
+        hass.succeed(f"grep -q 'wake_on_lan' {package}/extra_components")
+
     with subtest("Check that Home Assistant's web interface and API can be reached"):
+        wait_for_homeassistant(hass, cursor)
         hass.wait_for_open_port(8123)
         hass.succeed("curl --fail http://localhost:8123/lovelace")
-    with subtest("Toggle a binary sensor using MQTT"):
-        hass.wait_for_open_port(1883)
-        hass.succeed(
-            "mosquitto_pub -V mqttv5 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light"
-        )
+
     with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
         hass.wait_for_open_port(80)
         hass.succeed("curl --fail http://localhost:80/description.xml")
+
     with subtest("Check extra components are considered in systemd unit hardening"):
         hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
-    with subtest("Print log to ease debugging"):
-        output_log = hass.succeed("cat ${configDir}/home-assistant.log")
-        print("\n### home-assistant.log ###\n")
-        print(output_log + "\n")
 
-    # wait for home-assistant to fully boot
-    hass.sleep(30)
-    hass.wait_for_unit("home-assistant.service")
+    with subtest("Check service reloads when configuration changes"):
+      # store the old pid of the process
+      pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
+      cursor = get_journal_cursor(hass)
+      hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test")
+      new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
+      assert pid == new_pid, "The PID of the process should not change between process reloads"
+      wait_for_homeassistant(hass, cursor)
+
+    with subtest("check service restarts when package changes"):
+      pid = new_pid
+      cursor = get_journal_cursor(hass)
+      hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test")
+      new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
+      assert pid != new_pid, "The PID of the process shoudl change when the HA binary changes"
+      wait_for_homeassistant(hass, cursor)
 
     with subtest("Check that no errors were logged"):
+        output_log = hass.succeed("cat ${configDir}/home-assistant.log")
         assert "ERROR" not in output_log
 
-    # example line: 2020-06-20 10:01:32 DEBUG (MainThread) [homeassistant.components.mqtt] Received message on home-assistant/test: b'let_there_be_light'
-    with subtest("Check we received the mosquitto message"):
-        assert "let_there_be_light" in output_log
-
     with subtest("Check systemd unit hardening"):
-        hass.log(hass.succeed("systemctl show home-assistant.service"))
+        hass.log(hass.succeed("systemctl cat home-assistant.service"))
         hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
   '';
 })
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
index 2e92b4259a6ab..1de8f19267af8 100644
--- a/nixos/tests/hostname.nix
+++ b/nixos/tests/hostname.nix
@@ -20,7 +20,7 @@ let
           maintainers = [ primeos blitz ];
         };
 
-        machine = { lib, ... }: {
+        nodes.machine = { lib, ... }: {
           networking.hostName = hostName;
           networking.domain = domain;
 
diff --git a/nixos/tests/hound.nix b/nixos/tests/hound.nix
index 4f51db1de9dec..a9b036deb0dda 100644
--- a/nixos/tests/hound.nix
+++ b/nixos/tests/hound.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
   meta = with pkgs.lib.maintainers; {
     maintainers = [ grahamc ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.hound = {
       enable = true;
       config = ''
diff --git a/nixos/tests/hydra/default.nix b/nixos/tests/hydra/default.nix
index ef5e677953dcd..baf18afbc5690 100644
--- a/nixos/tests/hydra/default.nix
+++ b/nixos/tests/hydra/default.nix
@@ -11,7 +11,7 @@ let
   inherit (import ./common.nix { inherit system; }) baseConfig;
 
   hydraPkgs = {
-    inherit (pkgs) hydra-unstable;
+    inherit (pkgs) hydra_unstable;
   };
 
   makeHydraTest = with pkgs.lib; name: package: makeTest {
@@ -20,7 +20,7 @@ let
       maintainers = [ lewo ma27 ];
     };
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ baseConfig ];
       services.hydra = { inherit package; };
     };
diff --git a/nixos/tests/i3wm.nix b/nixos/tests/i3wm.nix
index 59b4ffe3986e2..b216650d8192b 100644
--- a/nixos/tests/i3wm.nix
+++ b/nixos/tests/i3wm.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ aszlig ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
     test-support.displayManager.auto.user = "alice";
     services.xserver.displayManager.defaultSession = lib.mkForce "none+i3";
diff --git a/nixos/tests/ihatemoney/default.nix b/nixos/tests/ihatemoney/default.nix
index 78278d2e86996..894a97d43d35e 100644
--- a/nixos/tests/ihatemoney/default.nix
+++ b/nixos/tests/ihatemoney/default.nix
@@ -7,7 +7,7 @@ let
   inherit (import ../../lib/testing-python.nix { inherit system pkgs; }) makeTest;
   f = backend: makeTest {
     name = "ihatemoney-${backend}";
-    machine = { nodes, lib, ... }: {
+    nodes.machine = { nodes, lib, ... }: {
       services.ihatemoney = {
         enable = true;
         enablePublicProjectCreation = true;
@@ -32,14 +32,7 @@ let
         };
       };
       # ihatemoney needs a local smtp server otherwise project creation just crashes
-      services.opensmtpd = {
-        enable = true;
-        serverConfiguration = ''
-          listen on lo
-          action foo relay
-          match from any for any action foo
-        '';
-      };
+      services.postfix.enable = true;
     };
     testScript = ''
       machine.wait_for_open_port(8000)
diff --git a/nixos/tests/incron.nix b/nixos/tests/incron.nix
index b22ee4c9a037a..c978ff27dfad5 100644
--- a/nixos/tests/incron.nix
+++ b/nixos/tests/incron.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
   name = "incron";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.incron.enable = true;
       services.incron.extraPackages = [ pkgs.coreutils ];
diff --git a/nixos/tests/initrd-network.nix b/nixos/tests/initrd-network.nix
index 14e7e7d40bc59..f2483b7393de4 100644
--- a/nixos/tests/initrd-network.nix
+++ b/nixos/tests/initrd-network.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
   meta.maintainers = [ pkgs.lib.maintainers.eelco ];
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
     boot.initrd.network.enable = true;
     boot.initrd.network.postCommands =
diff --git a/nixos/tests/initrd-secrets.nix b/nixos/tests/initrd-secrets.nix
index 113a9cebf7880..0f3f83b0904e3 100644
--- a/nixos/tests/initrd-secrets.nix
+++ b/nixos/tests/initrd-secrets.nix
@@ -11,7 +11,7 @@ let
 
     meta.maintainers = [ lib.maintainers.lheckemann ];
 
-    machine = { ... }: {
+    nodes.machine = { ... }: {
       virtualisation.useBootLoader = true;
       boot.initrd.secrets = {
         "/test" = secretInStore;
diff --git a/nixos/tests/input-remapper.nix b/nixos/tests/input-remapper.nix
new file mode 100644
index 0000000000000..1b0350063f7f2
--- /dev/null
+++ b/nixos/tests/input-remapper.nix
@@ -0,0 +1,52 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  {
+    name = "input-remapper";
+    meta = {
+      maintainers = with pkgs.lib.maintainers; [ LunNova ];
+    };
+
+    nodes.machine = { config, ... }:
+      let user = config.users.users.sybil; in
+      {
+        imports = [
+          ./common/user-account.nix
+          ./common/x11.nix
+        ];
+
+        services.xserver.enable = true;
+        services.input-remapper.enable = true;
+        users.users.sybil = { isNormalUser = true; group = "wheel"; };
+        test-support.displayManager.auto.user = user.name;
+        # workaround for pkexec not working in the test environment
+        # Error creating textual authentication agent:
+        #   Error opening current controlling terminal for the process (`/dev/tty'):
+        #   No such device or address
+        # passwordless pkexec with polkit module also doesn't work
+        # to allow the program to run, we replace pkexec with sudo
+        # and turn on passwordless sudo
+        # this is not correct in general but good enough for this test
+        security.sudo = { enable = true; wheelNeedsPassword = false; };
+        security.wrappers.pkexec = pkgs.lib.mkForce
+          {
+            setuid = true;
+            owner = "root";
+            group = "root";
+            source = "${pkgs.sudo}/bin/sudo";
+          };
+      };
+
+    enableOCR = true;
+
+    testScript = { nodes, ... }: ''
+      start_all()
+      machine.wait_for_x()
+
+      machine.succeed("systemctl status input-remapper.service")
+      machine.execute("su - sybil -c input-remapper-gtk >&2 &")
+
+      machine.wait_for_text("Input Remapper")
+      machine.wait_for_text("Preset")
+      machine.wait_for_text("Change Key")
+    '';
+  })
diff --git a/nixos/tests/installed-tests/appstream-qt.nix b/nixos/tests/installed-tests/appstream-qt.nix
new file mode 100644
index 0000000000000..d08187bfe4663
--- /dev/null
+++ b/nixos/tests/installed-tests/appstream-qt.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libsForQt5.appstream-qt;
+
+  testConfig = {
+    appstream.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/appstream.nix b/nixos/tests/installed-tests/appstream.nix
new file mode 100644
index 0000000000000..f71a095d4452f
--- /dev/null
+++ b/nixos/tests/installed-tests/appstream.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.appstream;
+
+  testConfig = {
+    appstream.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
index 08785e5e6669d..b81384aa8c0b5 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -43,7 +43,7 @@ let
             maintainers = tested.meta.maintainers;
           };
 
-          machine = { ... }: {
+          nodes.machine = { ... }: {
             imports = [
               testConfig
             ] ++ optional withX11 ../common/x11.nix;
@@ -84,12 +84,15 @@ let
 in
 
 {
+  appstream = callInstalledTest ./appstream.nix {};
+  appstream-qt = callInstalledTest ./appstream-qt.nix {};
   colord = callInstalledTest ./colord.nix {};
   flatpak = callInstalledTest ./flatpak.nix {};
   flatpak-builder = callInstalledTest ./flatpak-builder.nix {};
   fwupd = callInstalledTest ./fwupd.nix {};
   gcab = callInstalledTest ./gcab.nix {};
   gdk-pixbuf = callInstalledTest ./gdk-pixbuf.nix {};
+  geocode-glib = callInstalledTest ./geocode-glib.nix {};
   gjs = callInstalledTest ./gjs.nix {};
   glib-networking = callInstalledTest ./glib-networking.nix {};
   gnome-photos = callInstalledTest ./gnome-photos.nix {};
@@ -104,6 +107,5 @@ in
   malcontent = callInstalledTest ./malcontent.nix {};
   ostree = callInstalledTest ./ostree.nix {};
   pipewire = callInstalledTest ./pipewire.nix {};
-  power-profiles-daemon = callInstalledTest ./power-profiles-daemon.nix {};
   xdg-desktop-portal = callInstalledTest ./xdg-desktop-portal.nix {};
 }
diff --git a/nixos/tests/installed-tests/flatpak-builder.nix b/nixos/tests/installed-tests/flatpak-builder.nix
index 31b9f2b258fdf..41f4060fb69e5 100644
--- a/nixos/tests/installed-tests/flatpak-builder.nix
+++ b/nixos/tests/installed-tests/flatpak-builder.nix
@@ -6,6 +6,7 @@ makeInstalledTest {
   testConfig = {
     services.flatpak.enable = true;
     xdg.portal.enable = true;
+    xdg.portal.extraPortals = with pkgs; [ xdg-desktop-portal-gtk ];
     environment.systemPackages = with pkgs; [ flatpak-builder ] ++ flatpak-builder.installedTestsDependencies;
     virtualisation.diskSize = 2048;
   };
diff --git a/nixos/tests/installed-tests/geocode-glib.nix b/nixos/tests/installed-tests/geocode-glib.nix
new file mode 100644
index 0000000000000..fcb38c96ab0f6
--- /dev/null
+++ b/nixos/tests/installed-tests/geocode-glib.nix
@@ -0,0 +1,13 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  testConfig = {
+    i18n.supportedLocales = [
+      "en_US.UTF-8/UTF-8"
+      # The tests require this locale available.
+      "en_GB.UTF-8/UTF-8"
+    ];
+  };
+
+  tested = pkgs.geocode-glib;
+}
diff --git a/nixos/tests/installed-tests/gjs.nix b/nixos/tests/installed-tests/gjs.nix
index 1656e9de171b7..d12487cba2499 100644
--- a/nixos/tests/installed-tests/gjs.nix
+++ b/nixos/tests/installed-tests/gjs.nix
@@ -3,4 +3,10 @@
 makeInstalledTest {
   tested = pkgs.gjs;
   withX11 = true;
+
+  testConfig = {
+    environment.systemPackages = [
+      pkgs.gjs
+    ];
+  };
 }
diff --git a/nixos/tests/installed-tests/power-profiles-daemon.nix b/nixos/tests/installed-tests/power-profiles-daemon.nix
deleted file mode 100644
index 43629a0155d24..0000000000000
--- a/nixos/tests/installed-tests/power-profiles-daemon.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-{ pkgs, lib, makeInstalledTest, ... }:
-
-makeInstalledTest {
-  tested = pkgs.power-profiles-daemon;
-
-  testConfig = {
-    services.power-profiles-daemon.enable = true;
-  };
-}
diff --git a/nixos/tests/installer-systemd-stage-1.nix b/nixos/tests/installer-systemd-stage-1.nix
new file mode 100644
index 0000000000000..d02387ee80e09
--- /dev/null
+++ b/nixos/tests/installer-systemd-stage-1.nix
@@ -0,0 +1,33 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+{
+  # Some of these tests don't work with systemd stage 1 yet. Uncomment
+  # them when fixed.
+  inherit (import ./installer.nix { inherit system config pkgs; systemdStage1 = true; })
+    # bcache
+    # btrfsSimple
+    # btrfsSubvolDefault
+    # btrfsSubvols
+    # encryptedFSWithKeyfile
+    # grub1
+    # luksroot
+    # luksroot-format1
+    # luksroot-format2
+    # lvm
+    separateBoot
+    separateBootFat
+    simple
+    simpleLabels
+    simpleProvided
+    simpleSpecialised
+    simpleUefiGrub
+    simpleUefiGrubSpecialisation
+    simpleUefiSystemdBoot
+    # swraid
+    zfsroot
+    ;
+
+}
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index 5525c3117b797..8bef4fad3dd2d 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -1,6 +1,7 @@
 { system ? builtins.currentSystem,
   config ? {},
-  pkgs ? import ../.. { inherit system config; }
+  pkgs ? import ../.. { inherit system config; },
+  systemdStage1 ? false
 }:
 
 with import ../lib/testing-python.nix { inherit system pkgs; };
@@ -23,6 +24,8 @@ let
         # To ensure that we can rebuild the grub configuration on the nixos-rebuild
         system.extraDependencies = with pkgs; [ stdenvNoCC ];
 
+        ${optionalString systemdStage1 "boot.initrd.systemd.enable = true;"}
+
         ${optionalString (bootLoader == "grub") ''
           boot.loader.grub.version = ${toString grubVersion};
           ${optionalString (grubVersion == 1) ''
@@ -290,6 +293,8 @@ let
           virtualisation.cores = 8;
           virtualisation.memorySize = 1536;
 
+          boot.initrd.systemd.enable = systemdStage1;
+
           # Use a small /dev/vdb as the root disk for the
           # installer. This ensures the target disk (/dev/vda) is
           # the same during and after installation.
@@ -299,6 +304,13 @@ let
           virtualisation.qemu.diskInterface =
             if grubVersion == 1 then "scsi" else "virtio";
 
+          # We don't want to have any networking in the guest whatsoever.
+          # Also, if any vlans are enabled, the guest will reboot
+          # (with a different configuration for legacy reasons),
+          # and spend 5 minutes waiting for the vlan interface to show up
+          # (which will never happen).
+          virtualisation.vlans = [];
+
           boot.loader.systemd-boot.enable = mkIf (bootLoader == "systemd-boot") true;
 
           hardware.enableAllFirmware = mkForce false;
@@ -312,6 +324,8 @@ let
             desktop-file-utils
             docbook5
             docbook_xsl_ns
+            kmod.dev
+            libarchive.dev
             libxml2.bin
             libxslt.bin
             nixos-artwork.wallpapers.simple-dark-gray-bottom
@@ -568,7 +582,7 @@ in {
           "pvcreate /dev/vda1 /dev/vda2",
           "vgcreate MyVolGroup /dev/vda1 /dev/vda2",
           "lvcreate --size 1G --name swap MyVolGroup",
-          "lvcreate --size 3G --name nixos MyVolGroup",
+          "lvcreate --size 6G --name nixos MyVolGroup",
           "mkswap -f /dev/MyVolGroup/swap -L swap",
           "swapon -L swap",
           "mkfs.xfs -L nixos /dev/MyVolGroup/nixos",
@@ -687,6 +701,85 @@ in {
     '';
   };
 
+  bcachefsSimple = makeInstallerTest "bcachefs-simple" {
+    extraInstallerConfig = {
+      boot.supportedFilesystems = [ "bcachefs" ];
+    };
+
+    createPartitions = ''
+      machine.succeed(
+        "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+        + " mkpart primary ext2 1M 100MB"          # /boot
+        + " mkpart primary linux-swap 100M 1024M"  # swap
+        + " mkpart primary 1024M -1s",             # /
+        "udevadm settle",
+        "mkswap /dev/vda2 -L swap",
+        "swapon -L swap",
+        "mkfs.bcachefs -L root /dev/vda3",
+        "mount -t bcachefs /dev/vda3 /mnt",
+        "mkfs.ext3 -L boot /dev/vda1",
+        "mkdir -p /mnt/boot",
+        "mount /dev/vda1 /mnt/boot",
+      )
+    '';
+  };
+
+  bcachefsEncrypted = makeInstallerTest "bcachefs-encrypted" {
+    extraInstallerConfig = {
+      boot.supportedFilesystems = [ "bcachefs" ];
+      environment.systemPackages = with pkgs; [ keyutils ];
+    };
+
+    # We don't want to use the normal way of unlocking bcachefs defined in tasks/filesystems/bcachefs.nix.
+    # So, override initrd.postDeviceCommands completely and simply unlock with the predefined password.
+    extraConfig = ''
+      boot.initrd.postDeviceCommands = lib.mkForce "echo password | bcachefs unlock /dev/vda3";
+    '';
+
+    createPartitions = ''
+      machine.succeed(
+        "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+        + " mkpart primary ext2 1M 100MB"          # /boot
+        + " mkpart primary linux-swap 100M 1024M"  # swap
+        + " mkpart primary 1024M -1s",             # /
+        "udevadm settle",
+        "mkswap /dev/vda2 -L swap",
+        "swapon -L swap",
+        "keyctl link @u @s",
+        "echo password | mkfs.bcachefs -L root --encrypted /dev/vda3",
+        "echo password | bcachefs unlock /dev/vda3",
+        "mount -t bcachefs /dev/vda3 /mnt",
+        "mkfs.ext3 -L boot /dev/vda1",
+        "mkdir -p /mnt/boot",
+        "mount /dev/vda1 /mnt/boot",
+      )
+    '';
+  };
+
+  bcachefsMulti = makeInstallerTest "bcachefs-multi" {
+    extraInstallerConfig = {
+      boot.supportedFilesystems = [ "bcachefs" ];
+    };
+
+    createPartitions = ''
+      machine.succeed(
+        "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+        + " mkpart primary ext2 1M 100MB"          # /boot
+        + " mkpart primary linux-swap 100M 1024M"  # swap
+        + " mkpart primary 1024M 4096M"            # /
+        + " mkpart primary 4096M -1s",             # /
+        "udevadm settle",
+        "mkswap /dev/vda2 -L swap",
+        "swapon -L swap",
+        "mkfs.bcachefs -L root --metadata_replicas 2 --foreground_target ssd --promote_target ssd --background_target hdd --label ssd /dev/vda3 --label hdd /dev/vda4",
+        "mount -t bcachefs /dev/vda3:/dev/vda4 /mnt",
+        "mkfs.ext3 -L boot /dev/vda1",
+        "mkdir -p /mnt/boot",
+        "mount /dev/vda1 /mnt/boot",
+      )
+    '';
+  };
+
   # Test a basic install using GRUB 1.
   grub1 = makeInstallerTest "grub1" rec {
     createPartitions = ''
diff --git a/nixos/tests/invidious.nix b/nixos/tests/invidious.nix
index 8b831715a441f..582d1550fff1a 100644
--- a/nixos/tests/invidious.nix
+++ b/nixos/tests/invidious.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ sbruder ];
   };
 
-  machine = { config, lib, pkgs, ... }: {
+  nodes.machine = { config, lib, pkgs, ... }: {
     services.invidious = {
       enable = true;
     };
diff --git a/nixos/tests/ipfs.nix b/nixos/tests/ipfs.nix
index f8683b0a85809..024822745ada5 100644
--- a/nixos/tests/ipfs.nix
+++ b/nixos/tests/ipfs.nix
@@ -10,6 +10,15 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       # Also will add a unix domain socket socket API address, see module.
       startWhenNeeded = true;
       apiAddress = "/ip4/127.0.0.1/tcp/2324";
+      dataDir = "/mnt/ipfs";
+    };
+  };
+
+  nodes.fuse = { ... }: {
+    services.ipfs = {
+      enable = true;
+      apiAddress = "/ip4/127.0.0.1/tcp/2324";
+      autoMount = true;
     };
   };
 
@@ -35,5 +44,18 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     machine.succeed(
         f"ipfs --api /unix/run/ipfs.sock cat /ipfs/{ipfs_hash.strip()} | grep fnord2"
     )
+
+    # Test if setting dataDir works properly with the hardened systemd unit
+    machine.succeed("test -e /mnt/ipfs/config")
+    machine.succeed("test ! -e /var/lib/ipfs/")
+
+    # Test FUSE mountpoint
+    ipfs_hash = fuse.succeed(
+        "echo fnord3 | ipfs --api /ip4/127.0.0.1/tcp/2324 add --quieter"
+    )
+
+    # The FUSE mount functionality is broken as of v0.13.0.
+    # See https://github.com/ipfs/kubo/issues/9044.
+    # fuse.succeed(f"cat /ipfs/{ipfs_hash.strip()} | grep fnord3")
   '';
 })
diff --git a/nixos/tests/isso.nix b/nixos/tests/isso.nix
index 99dc8009ae064..f4560ba3e6359 100644
--- a/nixos/tests/isso.nix
+++ b/nixos/tests/isso.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ asbachb ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     services.isso = {
       enable = true;
       settings = {
@@ -22,7 +22,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   ''
     machine.wait_for_unit("isso.service")
 
-    machine.wait_for_open_port("${toString port}")
+    machine.wait_for_open_port(port)
 
     machine.succeed("curl --fail http://localhost:${toString port}/?uri")
     machine.succeed("curl --fail http://localhost:${toString port}/js/embed.min.js")
diff --git a/nixos/tests/jellyfin.nix b/nixos/tests/jellyfin.nix
index cae31a7192582..7d3097b586299 100644
--- a/nixos/tests/jellyfin.nix
+++ b/nixos/tests/jellyfin.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
     name = "jellyfin";
     meta.maintainers = with lib.maintainers; [ minijackson ];
 
-    machine =
+    nodes.machine =
       { ... }:
       {
         services.jellyfin.enable = true;
@@ -52,18 +52,18 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
             machine.succeed(api_post("/Startup/Complete"))
 
         with machine.nested("Can login"):
-            auth_result = machine.succeed(
+            auth_result_str = machine.succeed(
                 api_post(
                     "/Users/AuthenticateByName",
                     "${payloads.auth}",
                 )
             )
-            auth_result = json.loads(auth_result)
+            auth_result = json.loads(auth_result_str)
             auth_token = auth_result["AccessToken"]
             auth_header += f", Token={auth_token}"
 
-            sessions_result = machine.succeed(api_get("/Sessions"))
-            sessions_result = json.loads(sessions_result)
+            sessions_result_str = machine.succeed(api_get("/Sessions"))
+            sessions_result = json.loads(sessions_result_str)
 
             this_session = [
                 session for session in sessions_result if session["DeviceId"] == "1337"
@@ -71,8 +71,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
             if len(this_session) != 1:
                 raise Exception("Session not created")
 
-            me = machine.succeed(api_get("/Users/Me"))
-            me = json.loads(me)["Id"]
+            me_str = machine.succeed(api_get("/Users/Me"))
+            me = json.loads(me_str)["Id"]
 
         with machine.nested("Can add library"):
             tempdir = machine.succeed("mktemp -d -p /var/lib/jellyfin").strip()
@@ -100,8 +100,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
 
 
         def is_refreshed(_):
-            folders = machine.succeed(api_get("/Library/VirtualFolders"))
-            folders = json.loads(folders)
+            folders_str = machine.succeed(api_get("/Library/VirtualFolders"))
+            folders = json.loads(folders_str)
             print(folders)
             return all(folder["RefreshStatus"] == "Idle" for folder in folders)
 
@@ -116,10 +116,10 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
             def has_movie(_):
                 global items
 
-                items = machine.succeed(
+                items_str = machine.succeed(
                     api_get(f"/Users/{me}/Items?IncludeItemTypes=Movie&Recursive=true")
                 )
-                items = json.loads(items)["Items"]
+                items = json.loads(items_str)["Items"]
 
                 return len(items) == 1
 
@@ -127,8 +127,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
 
             video = items[0]["Id"]
 
-            item_info = machine.succeed(api_get(f"/Users/{me}/Items/{video}"))
-            item_info = json.loads(item_info)
+            item_info_str = machine.succeed(api_get(f"/Users/{me}/Items/{video}"))
+            item_info = json.loads(item_info_str)
 
             if item_info["Name"] != "Big Buck Bunny":
                 raise Exception("Jellyfin failed to properly identify file")
diff --git a/nixos/tests/jenkins.nix b/nixos/tests/jenkins.nix
index cb4207c6e7738..3f111426db388 100644
--- a/nixos/tests/jenkins.nix
+++ b/nixos/tests/jenkins.nix
@@ -18,6 +18,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           enable = true;
           jobBuilder = {
             enable = true;
+            accessUser = "admin";
+            accessTokenFile = "/var/lib/jenkins/secrets/initialAdminPassword";
             nixJobs = [
               { job = {
                   name = "job-1";
@@ -79,7 +81,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     in ''
     start_all()
 
-    master.wait_for_unit("jenkins")
+    master.wait_for_unit("default.target")
 
     assert "Authentication required" in master.succeed("curl http://localhost:8080")
 
@@ -90,20 +92,16 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
     slave.fail("systemctl is-enabled jenkins.service")
 
+    slave.succeed("java -fullversion")
+
     with subtest("jobs are declarative"):
         # Check that jobs are created on disk.
-        master.wait_for_unit("jenkins-job-builder")
-        master.wait_until_fails("systemctl is-active jenkins-job-builder")
         master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/job-1/config.xml")
         master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/folder-1/config.xml")
         master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/folder-1/jobs/job-2/config.xml")
 
-        # Wait until jenkins is ready, reload configuration and verify it also
-        # sees the jobs.
-        master.succeed("curl --fail ${jenkinsUrl}/cli")
-        master.succeed("curl ${jenkinsUrl}/jnlpJars/jenkins-cli.jar -O")
-        master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) reload-configuration")
-        out = master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
+        # Verify that jenkins also sees the jobs.
+        out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
         jobs = [x.strip() for x in out.splitlines()]
         # Seeing jobs inside folders requires the Folders plugin
         # (https://plugins.jenkins.io/cloudbees-folder/), which we don't have
@@ -115,15 +113,12 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         )
 
         # Check that jobs are removed from disk.
-        master.wait_for_unit("jenkins-job-builder")
-        master.wait_until_fails("systemctl is-active jenkins-job-builder")
         master.wait_until_fails("test -f /var/lib/jenkins/jobs/job-1/config.xml")
         master.wait_until_fails("test -f /var/lib/jenkins/jobs/folder-1/config.xml")
         master.wait_until_fails("test -f /var/lib/jenkins/jobs/folder-1/jobs/job-2/config.xml")
 
-        # Reload jenkins' configuration and verify it also sees the jobs as removed.
-        master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) reload-configuration")
-        out = master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
+        # Verify that jenkins also sees the jobs as removed.
+        out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
         jobs = [x.strip() for x in out.splitlines()]
         assert jobs == [], f"jobs != []: {jobs}"
   '';
diff --git a/nixos/tests/jibri.nix b/nixos/tests/jibri.nix
index af20e639d30ef..223120cdb2291 100644
--- a/nixos/tests/jibri.nix
+++ b/nixos/tests/jibri.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = teams.jitsi.members;
   };
 
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       virtualisation.memorySize = 5120;
 
       services.jitsi-meet = {
diff --git a/nixos/tests/jitsi-meet.nix b/nixos/tests/jitsi-meet.nix
index d95f7c2ea9eaa..c39cd19e1f0a7 100644
--- a/nixos/tests/jitsi-meet.nix
+++ b/nixos/tests/jitsi-meet.nix
@@ -21,9 +21,9 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         forceSSL = true;
       };
 
-      security.acme.email = "me@example.org";
       security.acme.acceptTerms = true;
-      security.acme.server = "https://example.com"; # self-signed only
+      security.acme.defaults.email = "me@example.org";
+      security.acme.defaults.server = "https://example.com"; # self-signed only
     };
   };
 
@@ -34,9 +34,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     server.wait_for_unit("prosody.service")
 
     server.wait_until_succeeds(
-        "journalctl -b -u jitsi-videobridge2 -o cat | grep -q 'Performed a successful health check'"
-    )
-    server.wait_until_succeeds(
         "journalctl -b -u prosody -o cat | grep -q 'Authenticated as focus@auth.server'"
     )
     server.wait_until_succeeds(
diff --git a/nixos/tests/k3s-single-node-docker.nix b/nixos/tests/k3s-single-node-docker.nix
deleted file mode 100644
index 7f3d15788b043..0000000000000
--- a/nixos/tests/k3s-single-node-docker.nix
+++ /dev/null
@@ -1,84 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ... }:
-
-  let
-    imageEnv = pkgs.buildEnv {
-      name = "k3s-pause-image-env";
-      paths = with pkgs; [ tini (hiPrio coreutils) busybox ];
-    };
-    pauseImage = pkgs.dockerTools.streamLayeredImage {
-      name = "test.local/pause";
-      tag = "local";
-      contents = imageEnv;
-      config.Entrypoint = [ "/bin/tini" "--" "/bin/sleep" "inf" ];
-    };
-    # Don't use the default service account because there's a race where it may
-    # not be created yet; make our own instead.
-    testPodYaml = pkgs.writeText "test.yml" ''
-      apiVersion: v1
-      kind: ServiceAccount
-      metadata:
-        name: test
-      ---
-      apiVersion: v1
-      kind: Pod
-      metadata:
-        name: test
-      spec:
-        serviceAccountName: test
-        containers:
-        - name: test
-          image: test.local/pause:local
-          imagePullPolicy: Never
-          command: ["sh", "-c", "sleep inf"]
-    '';
-  in
-  {
-    name = "k3s";
-    meta = with pkgs.lib.maintainers; {
-      maintainers = [ euank ];
-    };
-
-    machine = { pkgs, ... }: {
-      environment.systemPackages = with pkgs; [ k3s gzip ];
-
-      # k3s uses enough resources the default vm fails.
-      virtualisation.memorySize = 1536;
-      virtualisation.diskSize = 4096;
-
-      services.k3s = {
-        enable = true;
-        role = "server";
-        docker = true;
-        # Slightly reduce resource usage
-        extraFlags = "--no-deploy coredns,servicelb,traefik,local-storage,metrics-server --pause-image test.local/pause:local";
-      };
-
-      users.users = {
-        noprivs = {
-          isNormalUser = true;
-          description = "Can't access k3s by default";
-          password = "*";
-        };
-      };
-    };
-
-    testScript = ''
-      start_all()
-
-      machine.wait_for_unit("k3s")
-      machine.succeed("k3s kubectl cluster-info")
-      machine.fail("sudo -u noprivs k3s kubectl cluster-info")
-      # FIXME: this fails with the current nixos kernel config; once it passes, we should uncomment it
-      # machine.succeed("k3s check-config")
-
-      machine.succeed(
-          "${pauseImage} | docker load"
-      )
-
-      machine.succeed("k3s kubectl apply -f ${testPodYaml}")
-      machine.succeed("k3s kubectl wait --for 'condition=Ready' pod/test")
-      machine.succeed("k3s kubectl delete -f ${testPodYaml}")
-
-      machine.shutdown()
-    '';
-  })
diff --git a/nixos/tests/k3s-single-node.nix b/nixos/tests/k3s-single-node.nix
index d98f20d468cbc..fb6510ee087bc 100644
--- a/nixos/tests/k3s-single-node.nix
+++ b/nixos/tests/k3s-single-node.nix
@@ -38,7 +38,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
       maintainers = [ euank ];
     };
 
-    machine = { pkgs, ... }: {
+    nodes.machine = { pkgs, ... }: {
       environment.systemPackages = with pkgs; [ k3s gzip ];
 
       # k3s uses enough resources the default vm fails.
diff --git a/nixos/tests/kanidm.nix b/nixos/tests/kanidm.nix
new file mode 100644
index 0000000000000..d34f680f5224b
--- /dev/null
+++ b/nixos/tests/kanidm.nix
@@ -0,0 +1,75 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    certs = import ./common/acme/server/snakeoil-certs.nix;
+    serverDomain = certs.domain;
+  in
+  {
+    name = "kanidm";
+    meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi ];
+
+    nodes.server = { config, pkgs, lib, ... }: {
+      services.kanidm = {
+        enableServer = true;
+        serverSettings = {
+          origin = "https://${serverDomain}";
+          domain = serverDomain;
+          bindaddress = "[::1]:8443";
+          ldapbindaddress = "[::1]:636";
+        };
+      };
+
+      services.nginx = {
+        enable = true;
+        recommendedProxySettings = true;
+        virtualHosts."${serverDomain}" = {
+          forceSSL = true;
+          sslCertificate = certs."${serverDomain}".cert;
+          sslCertificateKey = certs."${serverDomain}".key;
+          locations."/".proxyPass = "http://[::1]:8443";
+        };
+      };
+
+      security.pki.certificateFiles = [ certs.ca.cert ];
+
+      networking.hosts."::1" = [ serverDomain ];
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      users.users.kanidm.shell = pkgs.bashInteractive;
+
+      environment.systemPackages = with pkgs; [ kanidm openldap ripgrep ];
+    };
+
+    nodes.client = { pkgs, nodes, ... }: {
+      services.kanidm = {
+        enableClient = true;
+        clientSettings = {
+          uri = "https://${serverDomain}";
+        };
+      };
+
+      networking.hosts."${nodes.server.config.networking.primaryIPAddress}" = [ serverDomain ];
+
+      security.pki.certificateFiles = [ certs.ca.cert ];
+    };
+
+    testScript = { nodes, ... }:
+      let
+        ldapBaseDN = builtins.concatStringsSep "," (map (s: "dc=" + s) (pkgs.lib.splitString "." serverDomain));
+
+        # We need access to the config file in the test script.
+        filteredConfig = pkgs.lib.converge
+          (pkgs.lib.filterAttrsRecursive (_: v: v != null))
+          nodes.server.config.services.kanidm.serverSettings;
+        serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
+
+      in
+      ''
+        start_all()
+        server.wait_for_unit("kanidm.service")
+        server.wait_until_succeeds("curl -sf https://${serverDomain} | grep Kanidm")
+        server.wait_until_succeeds("ldapsearch -H ldap://[::1]:636 -b '${ldapBaseDN}' -x '(name=test)'")
+        client.wait_until_succeeds("kanidm login -D anonymous && kanidm self whoami | grep anonymous@${serverDomain}")
+        (rv, result) = server.execute("kanidmd recover_account -d quiet -c ${serverConfigFile} -n admin 2>&1 | rg -o '[A-Za-z0-9]{48}'")
+        assert rv == 0
+      '';
+  })
diff --git a/nixos/tests/kbd-setfont-decompress.nix b/nixos/tests/kbd-setfont-decompress.nix
index c3a495afac840..810ef39cc11a3 100644
--- a/nixos/tests/kbd-setfont-decompress.nix
+++ b/nixos/tests/kbd-setfont-decompress.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
 
   meta.maintainers = with lib.maintainers; [ oxalica ];
 
-  machine = { ... }: {};
+  nodes.machine = { ... }: {};
 
   testScript = ''
     machine.succeed("gzip -cd ${pkgs.terminus_font}/share/consolefonts/ter-v16b.psf.gz >font.psf")
diff --git a/nixos/tests/kbd-update-search-paths-patch.nix b/nixos/tests/kbd-update-search-paths-patch.nix
index 2cdb12340b1bc..746a809c4cdf7 100644
--- a/nixos/tests/kbd-update-search-paths-patch.nix
+++ b/nixos/tests/kbd-update-search-paths-patch.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "kbd-update-search-paths-patch";
 
-  machine = { pkgs, options, ... }: {
+  nodes.machine = { pkgs, options, ... }: {
     console = {
       packages = options.console.packages.default ++ [ pkgs.terminus_font ];
     };
diff --git a/nixos/tests/keepassxc.nix b/nixos/tests/keepassxc.nix
index 685a200b31878..303be13304057 100644
--- a/nixos/tests/keepassxc.nix
+++ b/nixos/tests/keepassxc.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     maintainers = [ turion ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [
@@ -15,20 +15,54 @@ import ./make-test-python.nix ({ pkgs, ...} :
     ];
 
     services.xserver.enable = true;
+
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/163482
+    qt5 = {
+      enable = true;
+      platformTheme = "gnome";
+      style = "adwaita-dark";
+    };
+
     test-support.displayManager.auto.user = "alice";
-    environment.systemPackages = [ pkgs.keepassxc ];
+    environment.systemPackages = with pkgs; [
+      keepassxc
+      xdotool
+    ];
   };
 
   enableOCR = true;
 
-  testScript = { nodes, ... }: ''
-    start_all()
-    machine.wait_for_x()
+  testScript = { nodes, ... }: let
+    aliceDo = cmd: ''machine.succeed("su - alice -c '${cmd}' >&2 &");'';
+    in ''
+    with subtest("Ensure X starts"):
+        start_all()
+        machine.wait_for_x()
+
+    with subtest("Can create database and entry with CLI"):
+        ${aliceDo "keepassxc-cli db-create -k foo.keyfile foo.kdbx"}
+        ${aliceDo "keepassxc-cli add --no-password -k foo.keyfile foo.kdbx bar"}
+
+    with subtest("Ensure KeePassXC starts"):
+        # start KeePassXC window
+        ${aliceDo "keepassxc >&2 &"}
 
-    # start KeePassXC window
-    machine.execute("su - alice -c keepassxc >&2 &")
+        machine.wait_for_text("KeePassXC ${pkgs.keepassxc.version}")
+        machine.screenshot("KeePassXC")
 
-    machine.wait_for_text("KeePassXC ${pkgs.keepassxc.version}")
-    machine.screenshot("KeePassXC")
+    with subtest("Can open existing database"):
+        machine.send_key("ctrl-o")
+        machine.sleep(5)
+        # Regression #163482: keepassxc did not crash
+        machine.succeed("ps -e | grep keepassxc")
+        machine.wait_for_text("foo.kdbx")
+        machine.send_key("ret")
+        machine.sleep(1)
+        # Click on "Browse" button to select keyfile
+        machine.send_key("tab")
+        machine.send_chars("/home/alice/foo.keyfile")
+        machine.send_key("ret")
+        # Database is unlocked (doesn't have "[Locked]" in the title anymore)
+        machine.wait_for_text("foo.kdbx - KeePassXC")
   '';
 })
diff --git a/nixos/tests/kerberos/heimdal.nix b/nixos/tests/kerberos/heimdal.nix
index 391a61cc9a90b..47f9d0285aef7 100644
--- a/nixos/tests/kerberos/heimdal.nix
+++ b/nixos/tests/kerberos/heimdal.nix
@@ -1,6 +1,6 @@
 import ../make-test-python.nix ({pkgs, ...}: {
   name = "kerberos_server-heimdal";
-  machine = { config, libs, pkgs, ...}:
+  nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
       realms = {
diff --git a/nixos/tests/kerberos/mit.nix b/nixos/tests/kerberos/mit.nix
index 93b4020d49941..b475b7e4c92bb 100644
--- a/nixos/tests/kerberos/mit.nix
+++ b/nixos/tests/kerberos/mit.nix
@@ -1,6 +1,6 @@
 import ../make-test-python.nix ({pkgs, ...}: {
   name = "kerberos_server-mit";
-  machine = { config, libs, pkgs, ...}:
+  nodes.machine = { config, libs, pkgs, ...}:
   { services.kerberos_server =
     { enable = true;
       realms = {
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
index 45c5c1963a0db..04d52cf82e42b 100644
--- a/nixos/tests/kernel-generic.nix
+++ b/nixos/tests/kernel-generic.nix
@@ -12,7 +12,7 @@ let
       maintainers = [ nequissimus atemu ];
     };
 
-    machine = { ... }:
+    nodes.machine = { ... }:
       {
         boot.kernelPackages = linuxPackages;
       };
@@ -30,6 +30,7 @@ let
       linux_5_4_hardened
       linux_5_10_hardened
       linux_5_15_hardened
+      linux_5_18_hardened
 
       linux_testing;
   };
diff --git a/nixos/tests/kernel-latest-ath-user-regd.nix b/nixos/tests/kernel-latest-ath-user-regd.nix
index 11a3959e692e9..09e1da9d2affe 100644
--- a/nixos/tests/kernel-latest-ath-user-regd.nix
+++ b/nixos/tests/kernel-latest-ath-user-regd.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ veehaitch ];
   };
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
     {
       boot.kernelPackages = pkgs.linuxPackages_latest;
       networking.wireless.athUserRegulatoryDomain = true;
diff --git a/nixos/tests/kexec.nix b/nixos/tests/kexec.nix
index 010f3da49846a..3f5a6f521af0a 100644
--- a/nixos/tests/kexec.nix
+++ b/nixos/tests/kexec.nix
@@ -1,22 +1,49 @@
-# Test whether fast reboots via kexec work.
-
-import ./make-test-python.nix ({ pkgs, lib, ...} : {
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "kexec";
   meta = with lib.maintainers; {
-    maintainers = [ eelco ];
+    maintainers = [ flokli lassulus ];
+  };
+
+  nodes = {
+    node1 = { ... }: {
+      virtualisation.vlans = [ ];
+      virtualisation.memorySize = 4 * 1024;
+      virtualisation.useBootLoader = true;
+      virtualisation.useEFIBoot = true;
+      boot.loader.systemd-boot.enable = true;
+      boot.loader.efi.canTouchEfiVariables = true;
+    };
+
+    node2 = { modulesPath, ... }: {
+      virtualisation.vlans = [ ];
+      environment.systemPackages = [ pkgs.hello ];
+      imports = [
+        "${modulesPath}/installer/netboot/netboot-minimal.nix"
+      ];
+    };
   };
 
-  machine = { ... }:
-    { virtualisation.vlans = [ ]; };
+  testScript = { nodes, ... }: ''
+    # Test whether reboot via kexec works.
+    node1.wait_for_unit("multi-user.target")
+    node1.succeed('kexec --load /run/current-system/kernel --initrd /run/current-system/initrd --command-line "$(</proc/cmdline)"')
+    node1.execute("systemctl kexec >&2 &", check_return=False)
+    node1.connected = False
+    node1.connect()
+    node1.wait_for_unit("multi-user.target")
+
+    # Check if the machine with netboot-minimal.nix profile boots up
+    node2.wait_for_unit("multi-user.target")
+    node2.shutdown()
+
+    # Kexec node1 to the toplevel of node2 via the kexec-boot script
+    node1.succeed('touch /run/foo')
+    node1.fail('hello')
+    node1.execute('${nodes.node2.config.system.build.kexecTree}/kexec-boot', check_return=False)
+    node1.succeed('! test -e /run/foo')
+    node1.succeed('hello')
+    node1.succeed('[ "$(hostname)" = "node2" ]')
 
-  testScript =
-    ''
-      machine.wait_for_unit("multi-user.target")
-      machine.succeed('kexec --load /run/current-system/kernel --initrd /run/current-system/initrd --command-line "$(</proc/cmdline)"')
-      machine.execute("systemctl kexec >&2 &", check_return=False)
-      machine.connected = False
-      machine.connect()
-      machine.wait_for_unit("multi-user.target")
-      machine.shutdown()
-    '';
+    node1.shutdown()
+  '';
 })
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
index 1be3fed6acc9d..6ce136330d438 100644
--- a/nixos/tests/keycloak.nix
+++ b/nixos/tests/keycloak.nix
@@ -4,7 +4,7 @@
 
 let
   certs = import ./common/acme/server/snakeoil-certs.nix;
-  frontendUrl = "https://${certs.domain}/auth";
+  frontendUrl = "https://${certs.domain}";
   initialAdminPassword = "h4IhoJFnt2iQIR9";
 
   keycloakTest = import ./make-test-python.nix (
@@ -16,8 +16,7 @@ let
       };
 
       nodes = {
-        keycloak = { ... }: {
-
+        keycloak = { config, ... }: {
           security.pki.certificateFiles = [
             certs.ca.cert
           ];
@@ -28,19 +27,26 @@ let
 
           services.keycloak = {
             enable = true;
-            inherit frontendUrl initialAdminPassword;
-            sslCertificate = certs.${certs.domain}.cert;
-            sslCertificateKey = certs.${certs.domain}.key;
+            settings = {
+              hostname = certs.domain;
+            };
+            inherit initialAdminPassword;
+            sslCertificate = "${certs.${certs.domain}.cert}";
+            sslCertificateKey = "${certs.${certs.domain}.key}";
             database = {
               type = databaseType;
               username = "bogus";
-              passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
+              name = "also bogus";
+              passwordFile = "${pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"}";
             };
+            plugins = with config.services.keycloak.package.plugins; [
+              keycloak-discord
+              keycloak-metrics-spi
+            ];
           };
-
           environment.systemPackages = with pkgs; [
             xmlstarlet
-            libtidy
+            html-tidy
             jq
           ];
         };
@@ -96,14 +102,27 @@ let
         in ''
           keycloak.start()
           keycloak.wait_for_unit("keycloak.service")
+          keycloak.wait_for_open_port(443)
           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
+          """)
+
+          # Register the metrics SPI
           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"
+              "${pkgs.jre}/bin/keytool -import -alias snakeoil -file ${certs.ca.cert} -storepass aaaaaa -keystore cacert.jks -noprompt",
+              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh config credentials --server '${frontendUrl}' --realm master --user admin --password '${initialAdminPassword}'",
+              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh update events/config -s 'eventsEnabled=true' -s 'adminEventsEnabled=true' -s 'eventsListeners+=metrics-listener'",
+              "curl -sSf '${frontendUrl}/realms/master/metrics' | grep '^keycloak_admin_event_UPDATE'"
           )
 
           # Publish the realm, including a test OIDC client and user
@@ -127,7 +146,7 @@ let
           # 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",
+              "tidy -asxml -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",
           )
 
@@ -135,7 +154,7 @@ let
           # 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",
+              "tidy -asxml -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",
           )
 
@@ -156,5 +175,6 @@ let
 in
 {
   postgres = keycloakTest { databaseType = "postgresql"; };
+  mariadb = keycloakTest { databaseType = "mariadb"; };
   mysql = keycloakTest { databaseType = "mysql"; };
 }
diff --git a/nixos/tests/krb5/deprecated-config.nix b/nixos/tests/krb5/deprecated-config.nix
index 9a9cafd4b13e9..aca29ae6ca2b2 100644
--- a/nixos/tests/krb5/deprecated-config.nix
+++ b/nixos/tests/krb5/deprecated-config.nix
@@ -7,7 +7,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ eqyiel ];
   };
 
-  machine =
+  nodes.machine =
     { ... }: {
       krb5 = {
         enable = true;
diff --git a/nixos/tests/krb5/example-config.nix b/nixos/tests/krb5/example-config.nix
index 0932c71dd9705..1125b02f01ca8 100644
--- a/nixos/tests/krb5/example-config.nix
+++ b/nixos/tests/krb5/example-config.nix
@@ -7,7 +7,7 @@ import ../make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ eqyiel ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }: {
       krb5 = {
         enable = true;
diff --git a/nixos/tests/ksm.nix b/nixos/tests/ksm.nix
index 8f84b32020ab9..026d2ee85a24a 100644
--- a/nixos/tests/ksm.nix
+++ b/nixos/tests/ksm.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ lib, ...} :
     maintainers = [ rnhmjoj ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
 
     hardware.ksm.enable = true;
diff --git a/nixos/tests/kubernetes/base.nix b/nixos/tests/kubernetes/base.nix
index f0c72084be5c8..d4410beb937e6 100644
--- a/nixos/tests/kubernetes/base.nix
+++ b/nixos/tests/kubernetes/base.nix
@@ -18,7 +18,7 @@ let
         ${master.ip}  api.${domain}
         ${concatMapStringsSep "\n" (machineName: "${machines.${machineName}.ip}  ${machineName}.${domain}") (attrNames machines)}
       '';
-      kubectl = with pkgs; runCommand "wrap-kubectl" { buildInputs = [ makeWrapper ]; } ''
+      wrapKubectl = with pkgs; runCommand "wrap-kubectl" { buildInputs = [ makeWrapper ]; } ''
         mkdir -p $out/bin
         makeWrapper ${pkgs.kubernetes}/bin/kubectl $out/bin/kubectl --set KUBECONFIG "/etc/kubernetes/cluster-admin.kubeconfig"
       '';
@@ -48,7 +48,7 @@ let
                 };
               };
               programs.bash.enableCompletion = true;
-              environment.systemPackages = [ kubectl ];
+              environment.systemPackages = [ wrapKubectl ];
               services.flannel.iface = "eth1";
               services.kubernetes = {
                 proxy.hostname = "${masterName}.${domain}";
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
index 3fd1dd31f746b..6299b7ff988af 100644
--- a/nixos/tests/kubernetes/dns.nix
+++ b/nixos/tests/kubernetes/dns.nix
@@ -33,7 +33,11 @@ let
   redisImage = pkgs.dockerTools.buildImage {
     name = "redis";
     tag = "latest";
-    contents = [ pkgs.redis pkgs.bind.host ];
+    copyToRoot = pkgs.buildEnv {
+      name = "image-root";
+      pathsToLink = [ "/bin" ];
+      paths = [ pkgs.redis pkgs.bind.host ];
+    };
     config.Entrypoint = ["/bin/redis-server"];
   };
 
@@ -54,7 +58,11 @@ let
   probeImage = pkgs.dockerTools.buildImage {
     name = "probe";
     tag = "latest";
-    contents = [ pkgs.bind.host pkgs.busybox ];
+    copyToRoot = pkgs.buildEnv {
+      name = "image-root";
+      pathsToLink = [ "/bin" ];
+      paths = [ pkgs.bind.host pkgs.busybox ];
+    };
     config.Entrypoint = ["/bin/tail"];
   };
 
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
index ca73562256e47..779eafbb1d24c 100644
--- a/nixos/tests/kubernetes/rbac.nix
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -76,7 +76,7 @@ let
     }];
   });
 
-  kubectl = pkgs.runCommand "copy-kubectl" { buildInputs = [ pkgs.kubernetes ]; } ''
+  copyKubectl = pkgs.runCommand "copy-kubectl" { } ''
     mkdir -p $out/bin
     cp ${pkgs.kubernetes}/bin/kubectl $out/bin/kubectl
   '';
@@ -84,7 +84,11 @@ let
   kubectlImage = pkgs.dockerTools.buildImage {
     name = "kubectl";
     tag = "latest";
-    contents = [ kubectl pkgs.busybox kubectlPod2 ];
+    copyToRoot = pkgs.buildEnv {
+      name = "image-root";
+      pathsToLink = [ "/bin" ];
+      paths = [ copyKubectl pkgs.busybox kubectlPod2 ];
+    };
     config.Entrypoint = ["/bin/sh"];
   };
 
diff --git a/nixos/tests/libinput.nix b/nixos/tests/libinput.nix
index 2f84aaadcd0be..9b6fa159b999c 100644
--- a/nixos/tests/libinput.nix
+++ b/nixos/tests/libinput.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ ... }:
 {
   name = "libinput";
 
-  machine = { ... }:
+  nodes.machine = { ... }:
     {
       imports = [
         ./common/x11.nix
diff --git a/nixos/tests/libreddit.nix b/nixos/tests/libreddit.nix
index f7ef701d0865f..82a44cb4e9cb3 100644
--- a/nixos/tests/libreddit.nix
+++ b/nixos/tests/libreddit.nix
@@ -6,14 +6,16 @@ with lib;
   name = "libreddit";
   meta.maintainers = with maintainers; [ fab ];
 
-  nodes.machine =
-    { pkgs, ... }:
-    { services.libreddit.enable = true; };
+  nodes.machine = {
+    services.libreddit.enable = true;
+    # Test CAP_NET_BIND_SERVICE
+    services.libreddit.port = 80;
+  };
 
   testScript = ''
     machine.wait_for_unit("libreddit.service")
-    machine.wait_for_open_port("8080")
-    # The service wants to get data from https://www.reddit.com
-    machine.succeed("curl http://localhost:8080/")
+    machine.wait_for_open_port(80)
+    # Query a page that does not require Internet access
+    machine.succeed("curl --fail http://localhost:80/settings")
   '';
 })
diff --git a/nixos/tests/libresprite.nix b/nixos/tests/libresprite.nix
index 1a6210e3671ae..16d272acfa0fa 100644
--- a/nixos/tests/libresprite.nix
+++ b/nixos/tests/libresprite.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/libuiohook.nix b/nixos/tests/libuiohook.nix
new file mode 100644
index 0000000000000..66c5033d96886
--- /dev/null
+++ b/nixos/tests/libuiohook.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "libuiohook";
+  meta = with lib.maintainers; { maintainers = [ anoa ]; };
+
+  nodes.client = { nodes, ... }:
+      let user = nodes.client.config.users.users.alice;
+      in {
+        imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+        environment.systemPackages = [ pkgs.libuiohook.test ];
+
+        test-support.displayManager.auto.user = user.name;
+      };
+
+  testScript = { nodes, ... }:
+    let user = nodes.client.config.users.users.alice;
+    in ''
+      client.wait_for_x()
+      client.succeed("su - alice -c ${pkgs.libuiohook.test}/share/uiohook_tests >&2 &")
+    '';
+})
diff --git a/nixos/tests/lidarr.nix b/nixos/tests/lidarr.nix
index d3f83e5d9145b..7fbaea62f80e6 100644
--- a/nixos/tests/lidarr.nix
+++ b/nixos/tests/lidarr.nix
@@ -14,7 +14,7 @@ with lib;
     start_all()
 
     machine.wait_for_unit("lidarr.service")
-    machine.wait_for_open_port("8686")
+    machine.wait_for_open_port(8686)
     machine.succeed("curl --fail http://localhost:8686/")
   '';
 })
diff --git a/nixos/tests/lightdm.nix b/nixos/tests/lightdm.nix
index e98230ecb1794..94cebd4a630ab 100644
--- a/nixos/tests/lightdm.nix
+++ b/nixos/tests/lightdm.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ aszlig ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ./common/user-account.nix ];
     services.xserver.enable = true;
     services.xserver.displayManager.lightdm.enable = true;
diff --git a/nixos/tests/lighttpd.nix b/nixos/tests/lighttpd.nix
new file mode 100644
index 0000000000000..36e2745c55c15
--- /dev/null
+++ b/nixos/tests/lighttpd.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "lighttpd";
+  meta.maintainers = with lib.maintainers; [ bjornfor ];
+
+  nodes = {
+    server = {
+      services.lighttpd.enable = true;
+      services.lighttpd.document-root = pkgs.runCommand "document-root" {} ''
+        mkdir -p "$out"
+        echo "hello nixos test" > "$out/file.txt"
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("lighttpd.service")
+    res = server.succeed("curl --fail http://localhost/file.txt")
+    assert "hello nixos test" in res, f"bad server response: '{res}'"
+  '';
+})
diff --git a/nixos/tests/limesurvey.nix b/nixos/tests/limesurvey.nix
index b60e80be2444c..9a3193991f352 100644
--- a/nixos/tests/limesurvey.nix
+++ b/nixos/tests/limesurvey.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "limesurvey";
   meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.limesurvey = {
       enable = true;
       virtualHost = {
diff --git a/nixos/tests/litestream.nix b/nixos/tests/litestream.nix
index 886fbfef9cf55..f9d71c526e9e7 100644
--- a/nixos/tests/litestream.nix
+++ b/nixos/tests/litestream.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ jwygoda ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     { services.litestream = {
         enable = true;
diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix
index 4d1dcc8cc32da..2cff38d20059d 100644
--- a/nixos/tests/login.nix
+++ b/nixos/tests/login.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
     maintainers = [ eelco ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, lib, ... }:
     { boot.kernelPackages = lib.mkIf latestKernel pkgs.linuxPackages_latest;
       sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
@@ -29,11 +29,11 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
           machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
 
       with subtest("Log in as alice on a virtual console"):
-          machine.wait_until_tty_matches(2, "login: ")
+          machine.wait_until_tty_matches("2", "login: ")
           machine.send_chars("alice\n")
-          machine.wait_until_tty_matches(2, "login: alice")
+          machine.wait_until_tty_matches("2", "login: alice")
           machine.wait_until_succeeds("pgrep login")
-          machine.wait_until_tty_matches(2, "Password: ")
+          machine.wait_until_tty_matches("2", "Password: ")
           machine.send_chars("foobar\n")
           machine.wait_until_succeeds("pgrep -u alice bash")
           machine.send_chars("touch done\n")
diff --git a/nixos/tests/logrotate.nix b/nixos/tests/logrotate.nix
new file mode 100644
index 0000000000000..b0685f3af9ff1
--- /dev/null
+++ b/nixos/tests/logrotate.nix
@@ -0,0 +1,152 @@
+# Test logrotate service works and is enabled by default
+
+let
+  importTest = { ... }: {
+    services.logrotate.settings.import = {
+      olddir = false;
+    };
+  };
+
+in
+
+import ./make-test-python.nix ({ pkgs, ... }: rec {
+  name = "logrotate";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ martinetd ];
+  };
+
+  nodes = {
+    defaultMachine = { ... }: { };
+    failingMachine = { ... }: {
+      services.logrotate.configFile = pkgs.writeText "logrotate.conf" ''
+        # self-written config file
+        su notarealuser notagroupeither
+      '';
+    };
+    machine = { config, ... }: {
+      imports = [ importTest ];
+
+      services.logrotate.settings = {
+        # remove default frequency header and add another
+        header = {
+          frequency = null;
+          delaycompress = true;
+        };
+        # extra global setting... affecting nothing
+        last_line = {
+          global = true;
+          priority = 2000;
+          shred = true;
+        };
+        # using mail somewhere should add --mail to logrotate invokation
+        sendmail = {
+          mail = "user@domain.tld";
+        };
+        # postrotate should be suffixed by 'endscript'
+        postrotate = {
+          postrotate = "touch /dev/null";
+        };
+        # check checkConfig works as expected: there is nothing to check here
+        # except that the file build passes
+        checkConf = {
+          su = "root utmp";
+          createolddir = "0750 root utmp";
+          create = "root utmp";
+          "create " = "0750 root utmp";
+        };
+        # multiple paths should be aggregated
+        multipath = {
+          files = [ "file1" "file2" ];
+        };
+        # overriding imported path should keep existing attributes
+        # (e.g. olddir is still set)
+        import = {
+          notifempty = true;
+        };
+      };
+      # extraConfig compatibility - should be added to top level, early.
+      services.logrotate.extraConfig = ''
+        nomail
+      '';
+      # paths compatibility
+      services.logrotate.paths = {
+        compat_path = {
+          path = "compat_test_path";
+        };
+        # user/group should be grouped as 'su user group'
+        compat_user = {
+          user = config.users.users.root.name;
+          group = "root";
+        };
+        # extraConfig in path should be added to block
+        compat_extraConfig = {
+          extraConfig = "dateext";
+        };
+        # keep -> rotate
+        compat_keep = {
+          keep = 1;
+        };
+      };
+    };
+  };
+
+  testScript =
+    ''
+      with subtest("whether logrotate works"):
+          # we must rotate once first to create logrotate stamp
+          defaultMachine.succeed("systemctl start logrotate.service")
+          # we need to wait for console text once here to
+          # clear console buffer up to this point for next wait
+          defaultMachine.wait_for_console_text('logrotate.service: Deactivated successfully')
+
+          defaultMachine.succeed(
+              # wtmp is present in default config.
+              "rm -f /var/log/wtmp*",
+              # we need to give it at least 1MB
+              "dd if=/dev/zero of=/var/log/wtmp bs=2M count=1",
+
+              # move into the future and check rotation.
+              "date -s 'now + 1 month + 1 day'")
+          defaultMachine.wait_for_console_text('logrotate.service: Deactivated successfully')
+          defaultMachine.succeed(
+              # check rotate worked
+              "[ -e /var/log/wtmp.1 ]",
+          )
+      with subtest("default config does not have mail"):
+          defaultMachine.fail("systemctl cat logrotate.service | grep -- --mail")
+      with subtest("using mails adds mail option"):
+          machine.succeed("systemctl cat logrotate.service | grep -- --mail")
+      with subtest("check generated config matches expectation"):
+          machine.succeed(
+              # copy conf to /tmp/logrotate.conf for easy grep
+              "conf=$(systemctl cat logrotate | grep -oE '/nix/store[^ ]*logrotate.conf'); cp $conf /tmp/logrotate.conf",
+              "! grep weekly /tmp/logrotate.conf",
+              "grep -E '^delaycompress' /tmp/logrotate.conf",
+              "tail -n 1 /tmp/logrotate.conf | grep shred",
+              "sed -ne '/\"sendmail\" {/,/}/p' /tmp/logrotate.conf | grep 'mail user@domain.tld'",
+              "sed -ne '/\"postrotate\" {/,/}/p' /tmp/logrotate.conf | grep endscript",
+              "grep '\"file1\"\n\"file2\" {' /tmp/logrotate.conf",
+              "sed -ne '/\"import\" {/,/}/p' /tmp/logrotate.conf | grep noolddir",
+              "sed -ne '1,/^\"/p' /tmp/logrotate.conf | grep nomail",
+              "grep '\"compat_test_path\" {' /tmp/logrotate.conf",
+              "sed -ne '/\"compat_user\" {/,/}/p' /tmp/logrotate.conf | grep 'su root root'",
+              "sed -ne '/\"compat_extraConfig\" {/,/}/p' /tmp/logrotate.conf | grep dateext",
+              "[[ $(sed -ne '/\"compat_keep\" {/,/}/p' /tmp/logrotate.conf | grep -w rotate) = \"  rotate 1\" ]]",
+              "! sed -ne '/\"compat_keep\" {/,/}/p' /tmp/logrotate.conf | grep -w keep",
+          )
+          # also check configFile option
+          failingMachine.succeed(
+              "conf=$(systemctl cat logrotate | grep -oE '/nix/store[^ ]*logrotate.conf'); cp $conf /tmp/logrotate.conf",
+              "grep 'self-written config' /tmp/logrotate.conf",
+          )
+      with subtest("Check logrotate-checkconf service"):
+          machine.wait_for_unit("logrotate-checkconf.service")
+          # wait_for_unit also asserts for success, so wait for
+          # parent target instead and check manually.
+          failingMachine.wait_for_unit("multi-user.target")
+          info = failingMachine.get_unit_info("logrotate-checkconf.service")
+          if info["ActiveState"] != "failed":
+              raise Exception('logrotate-checkconf.service was not failed')
+
+    '';
+})
diff --git a/nixos/tests/loki.nix b/nixos/tests/loki.nix
index 0c6dff3fdf137..470f80e9db635 100644
--- a/nixos/tests/loki.nix
+++ b/nixos/tests/loki.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
     maintainers = [ willibutz ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.loki = {
       enable = true;
       configFile = "${pkgs.grafana-loki.src}/cmd/loki/loki-local-config.yaml";
diff --git a/nixos/tests/lorri/default.nix b/nixos/tests/lorri/default.nix
index c33c7503993da..209b87f9f26a3 100644
--- a/nixos/tests/lorri/default.nix
+++ b/nixos/tests/lorri/default.nix
@@ -1,5 +1,5 @@
 import ../make-test-python.nix {
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ../../modules/profiles/minimal.nix ];
     environment.systemPackages = [ pkgs.lorri ];
   };
diff --git a/nixos/tests/lvm2/default.nix b/nixos/tests/lvm2/default.nix
new file mode 100644
index 0000000000000..1eb9f572a4d6d
--- /dev/null
+++ b/nixos/tests/lvm2/default.nix
@@ -0,0 +1,45 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+, lib ? pkgs.lib
+, kernelVersionsToTest ? [ "4.19" "5.4" "5.10" "5.15" "latest" ]
+}:
+
+# For quickly running a test, the nixosTests.lvm2.lvm-thinpool-linux-latest attribute is recommended
+let
+  tests = let callTest = p: lib.flip (import p) { inherit system pkgs; }; in {
+    thinpool = { test = callTest ./thinpool.nix; kernelFilter = lib.id; };
+    # we would like to test all versions, but the kernel module currently does not compile against the other versions
+    vdo = { test = callTest ./vdo.nix; kernelFilter = lib.filter (v: v == "5.15"); };
+
+
+    # systemd in stage 1
+    raid-sd-stage-1 = {
+      test = callTest ./systemd-stage-1.nix;
+      kernelFilter = lib.id;
+      flavour = "raid";
+    };
+    thinpool-sd-stage-1 = {
+      test = callTest ./systemd-stage-1.nix;
+      kernelFilter = lib.id;
+      flavour = "thinpool";
+    };
+    vdo-sd-stage-1 = {
+      test = callTest ./systemd-stage-1.nix;
+      kernelFilter = lib.filter (v: v == "5.15");
+      flavour = "vdo";
+    };
+  };
+in
+lib.listToAttrs (
+  lib.filter (x: x.value != {}) (
+    lib.flip lib.concatMap kernelVersionsToTest (version:
+      let
+        v' = lib.replaceStrings [ "." ] [ "_" ] version;
+      in
+      lib.flip lib.mapAttrsToList tests (name: t:
+        lib.nameValuePair "lvm-${name}-linux-${v'}" (lib.optionalAttrs (builtins.elem version (t.kernelFilter kernelVersionsToTest)) (t.test ({ kernelPackages = pkgs."linuxPackages_${v'}"; } // builtins.removeAttrs t [ "test" "kernelFilter" ])))
+      )
+    )
+  )
+)
diff --git a/nixos/tests/lvm2/systemd-stage-1.nix b/nixos/tests/lvm2/systemd-stage-1.nix
new file mode 100644
index 0000000000000..617ba77b1796c
--- /dev/null
+++ b/nixos/tests/lvm2/systemd-stage-1.nix
@@ -0,0 +1,104 @@
+{ kernelPackages ? null, flavour }: let
+  preparationCode = {
+    raid = ''
+      machine.succeed("vgcreate test_vg /dev/vdc /dev/vdd")
+      machine.succeed("lvcreate -L 512M --type raid0 test_vg -n test_lv")
+    '';
+
+    thinpool = ''
+      machine.succeed("vgcreate test_vg /dev/vdc")
+      machine.succeed("lvcreate -L 512M -T test_vg/test_thin_pool")
+      machine.succeed("lvcreate -n test_lv -V 16G --thinpool test_thin_pool test_vg")
+    '';
+
+    vdo = ''
+      machine.succeed("vgcreate test_vg /dev/vdc")
+      machine.succeed("lvcreate --type vdo -n test_lv -L 6G -V 12G test_vg/vdo_pool_lv")
+    '';
+  }.${flavour};
+
+  extraConfig = {
+    raid = {
+      boot.initrd.kernelModules = [
+        "dm-raid"
+        "raid0"
+      ];
+    };
+
+    thinpool = {
+      services.lvm = {
+        boot.thin.enable = true;
+        dmeventd.enable = true;
+      };
+    };
+
+    vdo = {
+      services.lvm = {
+        boot.vdo.enable = true;
+        dmeventd.enable = true;
+      };
+    };
+  }.${flavour};
+
+  extraCheck = {
+    raid = ''
+      "test_lv" in machine.succeed("lvs --select segtype=raid0")
+    '';
+
+    thinpool = ''
+      "test_lv" in machine.succeed("lvs --select segtype=thin-pool")
+    '';
+
+    vdo = ''
+      "test_lv" in machine.succeed("lvs --select segtype=vdo")
+    '';
+  }.${flavour};
+
+in import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "lvm2-${flavour}-systemd-stage-1";
+  meta.maintainers = with pkgs.lib.maintainers; [ das_j ];
+
+  nodes.machine = { pkgs, lib, ... }: {
+    imports = [ extraConfig ];
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 8192 8192 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+
+    environment.systemPackages = with pkgs; [ e2fsprogs ]; # for mkfs.ext4
+    boot = {
+      initrd.systemd = {
+        enable = true;
+        emergencyAccess = true;
+      };
+      initrd.services.lvm.enable = true;
+      kernelPackages = lib.mkIf (kernelPackages != null) kernelPackages;
+    };
+
+    specialisation.boot-lvm.configuration.virtualisation.bootDevice = "/dev/test_vg/test_lv";
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    # Create a VG for the root
+    ${preparationCode}
+    machine.succeed("mkfs.ext4 /dev/test_vg/test_lv")
+    machine.succeed("mkdir -p /mnt && mount /dev/test_vg/test_lv /mnt && echo hello > /mnt/test && umount /mnt")
+
+    # Boot from LVM
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-lvm.conf")
+    machine.succeed("sync")
+    machine.crash()
+    machine.wait_for_unit("multi-user.target")
+
+    # Ensure we have successfully booted from LVM
+    assert "(initrd)" in machine.succeed("systemd-analyze")  # booted with systemd in stage 1
+    assert "/dev/mapper/test_vg-test_lv on / type ext4" in machine.succeed("mount")
+    assert "hello" in machine.succeed("cat /test")
+    ${extraCheck}
+  '';
+})
diff --git a/nixos/tests/lvm2/thinpool.nix b/nixos/tests/lvm2/thinpool.nix
new file mode 100644
index 0000000000000..82c6460a890a0
--- /dev/null
+++ b/nixos/tests/lvm2/thinpool.nix
@@ -0,0 +1,32 @@
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "lvm2-thinpool";
+  meta.maintainers = with pkgs.lib.maintainers; [ ajs124 ];
+
+  nodes.machine = { pkgs, lib, ... }: {
+    virtualisation.emptyDiskImages = [ 4096 ];
+    services.lvm = {
+      boot.thin.enable = true;
+      dmeventd.enable = true;
+    };
+    environment.systemPackages = with pkgs; [ xfsprogs ];
+    environment.etc."lvm/lvm.conf".text = ''
+      activation/thin_pool_autoextend_percent = 10
+      activation/thin_pool_autoextend_threshold = 80
+    '';
+    boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+  };
+
+  testScript = ''
+    machine.succeed("vgcreate test_vg /dev/vdb")
+    machine.succeed("lvcreate -L 512M -T test_vg/test_thin_pool")
+    machine.succeed("lvcreate -n test_lv -V 16G --thinpool test_thin_pool test_vg")
+    machine.succeed("mkfs.xfs /dev/test_vg/test_lv")
+    machine.succeed("mkdir /mnt; mount /dev/test_vg/test_lv /mnt")
+    assert "/dev/mapper/test_vg-test_lv" == machine.succeed("findmnt -no SOURCE /mnt").strip()
+    machine.succeed("dd if=/dev/zero of=/mnt/empty.file bs=1M count=1024")
+    machine.succeed("journalctl -u dm-event.service | grep \"successfully resized\"")
+    machine.succeed("umount /mnt")
+    machine.succeed("vgchange -a n")
+  '';
+})
diff --git a/nixos/tests/lvm2/vdo.nix b/nixos/tests/lvm2/vdo.nix
new file mode 100644
index 0000000000000..5b014c2f72223
--- /dev/null
+++ b/nixos/tests/lvm2/vdo.nix
@@ -0,0 +1,27 @@
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "lvm2-vdo";
+  meta.maintainers = with pkgs.lib.maintainers; [ ajs124 ];
+
+  nodes.machine = { pkgs, lib, ... }: {
+    # Minimum required size for VDO volume: 5063921664 bytes
+    virtualisation.emptyDiskImages = [ 8192 ];
+    services.lvm = {
+      boot.vdo.enable = true;
+      dmeventd.enable = true;
+    };
+    environment.systemPackages = with pkgs; [ xfsprogs ];
+    boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+  };
+
+  testScript = ''
+    machine.succeed("vgcreate test_vg /dev/vdb")
+    machine.succeed("lvcreate --type vdo -n vdo_lv -L 6G -V 12G test_vg/vdo_pool_lv")
+    machine.succeed("mkfs.xfs -K /dev/test_vg/vdo_lv")
+    machine.succeed("mkdir /mnt; mount /dev/test_vg/vdo_lv /mnt")
+    assert "/dev/mapper/test_vg-vdo_lv" == machine.succeed("findmnt -no SOURCE /mnt").strip()
+    machine.succeed("umount /mnt")
+    machine.succeed("vdostats")
+    machine.succeed("vgchange -a n")
+  '';
+})
diff --git a/nixos/tests/lxd-image-server.nix b/nixos/tests/lxd-image-server.nix
index 9f060fed38d87..072f4570c2c9f 100644
--- a/nixos/tests/lxd-image-server.nix
+++ b/nixos/tests/lxd-image-server.nix
@@ -1,57 +1,24 @@
-import ./make-test-python.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, lib, ... } :
 
 let
-  # Since we don't have access to the internet during the tests, we have to
-  # pre-fetch lxd containers beforehand.
-  #
-  # I've chosen to import Alpine Linux, because its image is turbo-tiny and,
-  # generally, sufficient for our tests.
-  alpine-meta = pkgs.fetchurl {
-    url = "https://tarballs.nixos.org/alpine/3.12/lxd.tar.xz";
-    hash = "sha256-1tcKaO9lOkvqfmG/7FMbfAEToAuFy2YMewS8ysBKuLA=";
-  };
-
-  alpine-rootfs = pkgs.fetchurl {
-    url = "https://tarballs.nixos.org/alpine/3.12/rootfs.tar.xz";
-    hash = "sha256-Tba9sSoaiMtQLY45u7p5DMqXTSDgs/763L/SQp0bkCA=";
+  lxd-image = import ../release.nix {
+    configuration = {
+      # Building documentation makes the test unnecessarily take a longer time:
+      documentation.enable = lib.mkForce false;
+    };
   };
 
-  lxd-config = pkgs.writeText "config.yaml" ''
-    storage_pools:
-      - name: default
-        driver: dir
-        config:
-          source: /var/lxd-pool
-
-    networks:
-      - name: lxdbr0
-        type: bridge
-        config:
-          ipv4.address: auto
-          ipv6.address: none
-
-    profiles:
-      - name: default
-        devices:
-          eth0:
-            name: eth0
-            network: lxdbr0
-            type: nic
-          root:
-            path: /
-            pool: default
-            type: disk
-  '';
-
+  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.system};
+  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.system};
 
 in {
   name = "lxd-image-server";
 
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ mkg20001 ];
+    maintainers = [ mkg20001 patryk27 ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     virtualisation = {
       cores = 2;
 
@@ -100,20 +67,20 @@ in {
     # lxd expects the pool's directory to already exist
     machine.succeed("mkdir /var/lxd-pool")
 
-
     machine.succeed(
-        "cat ${lxd-config} | lxd init --preseed"
+        "cat ${./common/lxd/config.yaml} | lxd init --preseed"
     )
 
     machine.succeed(
-        "lxc image import ${alpine-meta} ${alpine-rootfs} --alias alpine"
+        "lxc image import ${lxd-image-metadata}/*/*.tar.xz ${lxd-image-rootfs}/*/*.tar.xz --alias nixos"
     )
 
-    loc = "/var/www/simplestreams/images/iats/alpine/amd64/default/v1"
+    loc = "/var/www/simplestreams/images/iats/nixos/amd64/default/v1"
 
     with subtest("push image to server"):
-        machine.succeed("lxc launch alpine test")
-        machine.succeed("lxc stop test")
+        machine.succeed("lxc launch nixos test")
+        machine.sleep(5)
+        machine.succeed("lxc stop -f test")
         machine.succeed("lxc publish --public test --alias=testimg")
         machine.succeed("lxc image export testimg")
         machine.succeed("ls >&2")
diff --git a/nixos/tests/lxd-image.nix b/nixos/tests/lxd-image.nix
deleted file mode 100644
index 096b9d9aba906..0000000000000
--- a/nixos/tests/lxd-image.nix
+++ /dev/null
@@ -1,89 +0,0 @@
-# This test ensures that the nixOS lxd images builds and functions properly
-# It has been extracted from `lxd.nix` to seperate failures of just the image and the lxd software
-
-import ./make-test-python.nix ({ pkgs, ...} : let
-  release = import ../release.nix {
-    /* configuration = {
-      environment.systemPackages = with pkgs; [ stdenv ]; # inject stdenv so rebuild test works
-    }; */
-  };
-
-  metadata = release.lxdMeta.${pkgs.system};
-  image = release.lxdImage.${pkgs.system};
-
-  lxd-config = pkgs.writeText "config.yaml" ''
-    storage_pools:
-      - name: default
-        driver: dir
-        config:
-          source: /var/lxd-pool
-
-    networks:
-      - name: lxdbr0
-        type: bridge
-        config:
-          ipv4.address: auto
-          ipv6.address: none
-
-    profiles:
-      - name: default
-        devices:
-          eth0:
-            name: eth0
-            network: lxdbr0
-            type: nic
-          root:
-            path: /
-            pool: default
-            type: disk
-  '';
-in {
-  name = "lxd-image";
-
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ mkg20001 ];
-  };
-
-  machine = { lib, ... }: {
-    virtualisation = {
-      # disk full otherwise
-      diskSize = 2048;
-
-      lxc.lxcfs.enable = true;
-      lxd.enable = true;
-    };
-  };
-
-  testScript = ''
-    machine.wait_for_unit("sockets.target")
-    machine.wait_for_unit("lxd.service")
-    machine.wait_for_file("/var/lib/lxd/unix.socket")
-
-    # It takes additional second for lxd to settle
-    machine.sleep(1)
-
-    # lxd expects the pool's directory to already exist
-    machine.succeed("mkdir /var/lxd-pool")
-
-    machine.succeed(
-        "cat ${lxd-config} | lxd init --preseed"
-    )
-
-    # TODO: test custom built container aswell
-
-    with subtest("importing container works"):
-        machine.succeed("lxc image import ${metadata}/*/*.tar.xz ${image}/*/*.tar.xz --alias nixos")
-
-    with subtest("launching container works"):
-        machine.succeed("lxc launch nixos machine -c security.nesting=true")
-        # make sure machine boots up properly
-        machine.sleep(5)
-
-    with subtest("container shell works"):
-        machine.succeed("echo true | lxc exec machine /run/current-system/sw/bin/bash -")
-        machine.succeed("lxc exec machine /run/current-system/sw/bin/true")
-
-    # with subtest("rebuilding works"):
-    #     machine.succeed("lxc exec machine /run/current-system/sw/bin/nixos-rebuild switch")
-  '';
-})
diff --git a/nixos/tests/lxd-nftables.nix b/nixos/tests/lxd-nftables.nix
index a62d5a3064dfb..2930650015679 100644
--- a/nixos/tests/lxd-nftables.nix
+++ b/nixos/tests/lxd-nftables.nix
@@ -12,7 +12,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ patryk27 ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     virtualisation = {
       lxd.enable = true;
     };
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd.nix
index 1a3b84a85cf68..15d16564d641c 100644
--- a/nixos/tests/lxd.nix
+++ b/nixos/tests/lxd.nix
@@ -1,48 +1,18 @@
-import ./make-test-python.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, lib, ... } :
 
 let
-  # Since we don't have access to the internet during the tests, we have to
-  # pre-fetch lxd containers beforehand.
-  #
-  # I've chosen to import Alpine Linux, because its image is turbo-tiny and,
-  # generally, sufficient for our tests.
-  alpine-meta = pkgs.fetchurl {
-    url = "https://tarballs.nixos.org/alpine/3.12/lxd.tar.xz";
-    hash = "sha256-1tcKaO9lOkvqfmG/7FMbfAEToAuFy2YMewS8ysBKuLA=";
-  };
+  lxd-image = import ../release.nix {
+    configuration = {
+      # Building documentation makes the test unnecessarily take a longer time:
+      documentation.enable = lib.mkForce false;
 
-  alpine-rootfs = pkgs.fetchurl {
-    url = "https://tarballs.nixos.org/alpine/3.12/rootfs.tar.xz";
-    hash = "sha256-Tba9sSoaiMtQLY45u7p5DMqXTSDgs/763L/SQp0bkCA=";
+      # Our tests require `grep` & friends:
+      environment.systemPackages = with pkgs; [ busybox ];
+    };
   };
 
-  lxd-config = pkgs.writeText "config.yaml" ''
-    storage_pools:
-      - name: default
-        driver: dir
-        config:
-          source: /var/lxd-pool
-
-    networks:
-      - name: lxdbr0
-        type: bridge
-        config:
-          ipv4.address: auto
-          ipv6.address: none
-
-    profiles:
-      - name: default
-        devices:
-          eth0:
-            name: eth0
-            network: lxdbr0
-            type: nic
-          root:
-            path: /
-            pool: default
-            type: disk
-  '';
-
+  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.system};
+  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.system};
 
 in {
   name = "lxd";
@@ -51,8 +21,10 @@ in {
     maintainers = [ patryk27 ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     virtualisation = {
+      diskSize = 2048;
+
       # Since we're testing `limits.cpu`, we've gotta have a known number of
       # cores to lean on
       cores = 2;
@@ -77,61 +49,66 @@ in {
     machine.succeed("mkdir /var/lxd-pool")
 
     machine.succeed(
-        "cat ${lxd-config} | lxd init --preseed"
+        "cat ${./common/lxd/config.yaml} | lxd init --preseed"
     )
 
     machine.succeed(
-        "lxc image import ${alpine-meta} ${alpine-rootfs} --alias alpine"
+        "lxc image import ${lxd-image-metadata}/*/*.tar.xz ${lxd-image-rootfs}/*/*.tar.xz --alias nixos"
     )
 
-    with subtest("Containers can be launched and destroyed"):
-        machine.succeed("lxc launch alpine test")
-        machine.succeed("lxc exec test true")
-        machine.succeed("lxc delete -f test")
+    with subtest("Container can be managed"):
+        machine.succeed("lxc launch nixos container")
+        machine.sleep(5)
+        machine.succeed("echo true | lxc exec container /run/current-system/sw/bin/bash -")
+        machine.succeed("lxc exec container true")
+        machine.succeed("lxc delete -f container")
 
-    with subtest("Containers are being mounted with lxcfs inside"):
-        machine.succeed("lxc launch alpine test")
+    with subtest("Container is mounted with lxcfs inside"):
+        machine.succeed("lxc launch nixos container")
+        machine.sleep(5)
 
         ## ---------- ##
         ## limits.cpu ##
 
-        machine.succeed("lxc config set test limits.cpu 1")
-        machine.succeed("lxc restart test")
+        machine.succeed("lxc config set container limits.cpu 1")
+        machine.succeed("lxc restart container")
+        machine.sleep(5)
 
-        # Since Alpine doesn't have `nproc` pre-installed, we've gotta resort
-        # to the primal methods
         assert (
             "1"
-            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+            == machine.succeed("lxc exec container grep -- -c ^processor /proc/cpuinfo").strip()
         )
 
-        machine.succeed("lxc config set test limits.cpu 2")
-        machine.succeed("lxc restart test")
+        machine.succeed("lxc config set container limits.cpu 2")
+        machine.succeed("lxc restart container")
+        machine.sleep(5)
 
         assert (
             "2"
-            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+            == machine.succeed("lxc exec container grep -- -c ^processor /proc/cpuinfo").strip()
         )
 
         ## ------------- ##
         ## limits.memory ##
 
-        machine.succeed("lxc config set test limits.memory 64MB")
-        machine.succeed("lxc restart test")
+        machine.succeed("lxc config set container limits.memory 64MB")
+        machine.succeed("lxc restart container")
+        machine.sleep(5)
 
         assert (
             "MemTotal:          62500 kB"
-            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+            == machine.succeed("lxc exec container grep -- MemTotal /proc/meminfo").strip()
         )
 
-        machine.succeed("lxc config set test limits.memory 128MB")
-        machine.succeed("lxc restart test")
+        machine.succeed("lxc config set container limits.memory 128MB")
+        machine.succeed("lxc restart container")
+        machine.sleep(5)
 
         assert (
             "MemTotal:         125000 kB"
-            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+            == machine.succeed("lxc exec container grep -- MemTotal /proc/meminfo").strip()
         )
 
-        machine.succeed("lxc delete -f test")
+        machine.succeed("lxc delete -f container")
   '';
 })
diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy.nix
index 581748c1fa59b..b9d0416482da1 100644
--- a/nixos/tests/maddy.nix
+++ b/nixos/tests/maddy.nix
@@ -49,7 +49,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     server.wait_for_open_port(143)
     server.wait_for_open_port(587)
 
-    server.succeed("echo test | maddyctl creds create postmaster@server")
+    server.succeed("maddyctl creds create --password test postmaster@server")
     server.succeed("maddyctl imap-acct create postmaster@server")
 
     client.succeed("send-testmail")
diff --git a/nixos/tests/maestral.nix b/nixos/tests/maestral.nix
new file mode 100644
index 0000000000000..ba2e0b2f3baab
--- /dev/null
+++ b/nixos/tests/maestral.nix
@@ -0,0 +1,72 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "maestral";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ peterhoeg ];
+  };
+
+  nodes =
+    let
+      common = attrs:
+        pkgs.lib.recursiveUpdate
+          {
+            imports = [ ./common/user-account.nix ];
+            systemd.user.services.maestral = {
+              description = "Maestral Dropbox Client";
+              serviceConfig.Type = "exec";
+            };
+          }
+          attrs;
+
+    in
+    {
+      cli = { ... }: common {
+        systemd.user.services.maestral = {
+          wantedBy = [ "default.target" ];
+          serviceConfig.ExecStart = "${pkgs.maestral}/bin/maestral start --foreground";
+        };
+      };
+
+      gui = { ... }: common {
+        services.xserver = {
+          enable = true;
+          displayManager.sddm.enable = true;
+          displayManager.defaultSession = "plasma";
+          desktopManager.plasma5.enable = true;
+          desktopManager.plasma5.runUsingSystemd = true;
+          displayManager.autoLogin = {
+            enable = true;
+            user = "alice";
+          };
+        };
+
+        systemd.user.services = {
+          maestral = {
+            wantedBy = [ "graphical-session.target" ];
+            serviceConfig.ExecStart = "${pkgs.maestral-gui}/bin/maestral_qt";
+          };
+          # PowerDevil doesn't like our VM
+          plasma-powerdevil.enable = false;
+        };
+      };
+    };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.cli.config.users.users.alice;
+    in
+    ''
+      start_all()
+
+      with subtest("CLI"):
+        # we need SOME way to give the user an active login session
+        cli.execute("loginctl enable-linger ${user.name}")
+        cli.systemctl("start user@${toString user.uid}")
+        cli.wait_for_unit("maestral.service", "${user.name}")
+
+      with subtest("GUI"):
+        gui.wait_for_x()
+        gui.succeed("xauth merge ${user.home}/.Xauthority")
+        gui.wait_for_window("^Desktop ")
+        gui.wait_for_unit("maestral.service", "${user.name}")
+    '';
+})
diff --git a/nixos/tests/magnetico.nix b/nixos/tests/magnetico.nix
index 8433a974f453b..ee84aacaf7a74 100644
--- a/nixos/tests/magnetico.nix
+++ b/nixos/tests/magnetico.nix
@@ -9,7 +9,7 @@ in
     maintainers = [ rnhmjoj ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
 
     networking.firewall.allowedTCPPorts = [ 9000 ];
diff --git a/nixos/tests/mailcatcher.nix b/nixos/tests/mailcatcher.nix
index a55fba8a9950b..627ef56617e9b 100644
--- a/nixos/tests/mailcatcher.nix
+++ b/nixos/tests/mailcatcher.nix
@@ -4,13 +4,18 @@ import ./make-test-python.nix ({ lib, ... }:
   name = "mailcatcher";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
     {
       services.mailcatcher.enable = true;
 
-      services.ssmtp.enable = true;
-      services.ssmtp.hostName = "localhost:1025";
+      programs.msmtp = {
+        enable = true;
+        accounts.default = {
+          host = "localhost";
+          port = 1025;
+        };
+      };
 
       environment.systemPackages = [ pkgs.mailutils ];
     };
@@ -19,7 +24,7 @@ import ./make-test-python.nix ({ lib, ... }:
     start_all()
 
     machine.wait_for_unit("mailcatcher.service")
-    machine.wait_for_open_port("1025")
+    machine.wait_for_open_port(1025)
     machine.succeed(
         'echo "this is the body of the email" | mail -s "subject" root@example.org'
     )
diff --git a/nixos/tests/mailhog.nix b/nixos/tests/mailhog.nix
index aece57178dd17..e3c2da37a3c8a 100644
--- a/nixos/tests/mailhog.nix
+++ b/nixos/tests/mailhog.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ lib, ... }: {
   name = "mailhog";
   meta.maintainers = with lib.maintainers; [ jojosch ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.mailhog.enable = true;
 
     environment.systemPackages = with pkgs; [ swaks ];
@@ -12,8 +12,8 @@ import ./make-test-python.nix ({ lib, ... }: {
     start_all()
 
     machine.wait_for_unit("mailhog.service")
-    machine.wait_for_open_port("1025")
-    machine.wait_for_open_port("8025")
+    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'
     )
diff --git a/nixos/tests/matomo.nix b/nixos/tests/matomo.nix
index f6b0845749cde..526a24fc4db75 100644
--- a/nixos/tests/matomo.nix
+++ b/nixos/tests/matomo.nix
@@ -7,7 +7,7 @@ with pkgs.lib;
 let
   matomoTest = package:
   makeTest {
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       services.matomo = {
         package = package;
         enable = true;
diff --git a/nixos/tests/matrix-appservice-irc.nix b/nixos/tests/matrix/appservice-irc.nix
index e1da410af0607..78c53024ca6c4 100644
--- a/nixos/tests/matrix-appservice-irc.nix
+++ b/nixos/tests/matrix/appservice-irc.nix
@@ -1,6 +1,6 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ../make-test-python.nix ({ pkgs, ... }:
   let
-    homeserverUrl = "http://homeserver:8448";
+    homeserverUrl = "http://homeserver:8008";
   in
   {
     name = "matrix-appservice-irc";
@@ -14,28 +14,35 @@ import ./make-test-python.nix ({ pkgs, ... }:
         specialisation.running.configuration = {
           services.matrix-synapse = {
             enable = true;
-            database_type = "sqlite3";
-            app_service_config_files = [ "/registration.yml" ];
-
-            enable_registration = true;
-
-            listeners = [
-              # The default but tls=false
-              {
-                "bind_address" = "";
-                "port" = 8448;
-                "resources" = [
-                  { "compress" = true; "names" = [ "client" ]; }
-                  { "compress" = false; "names" = [ "federation" ]; }
+            settings = {
+              database.name = "sqlite3";
+              app_service_config_files = [ "/registration.yml" ];
+
+              enable_registration = true;
+
+              # don't use this in production, always use some form of verification
+              enable_registration_without_verification = true;
+
+              listeners = [ {
+                # The default but tls=false
+                bind_addresses = [
+                  "0.0.0.0"
                 ];
-                "tls" = false;
-                "type" = "http";
-                "x_forwarded" = false;
-              }
-            ];
+                port = 8008;
+                resources = [ {
+                  "compress" = true;
+                  "names" = [ "client" ];
+                } {
+                  "compress" = false;
+                  "names" = [ "federation" ];
+                } ];
+                tls = false;
+                type = "http";
+              } ];
+            };
           };
 
-          networking.firewall.allowedTCPPorts = [ 8448 ];
+          networking.firewall.allowedTCPPorts = [ 8008 ];
         };
       };
 
@@ -186,6 +193,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
     testScript = ''
       import pathlib
+      import os
 
       start_all()
 
@@ -199,7 +207,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
       with subtest("copy the registration file"):
           appservice.copy_from_vm("/var/lib/matrix-appservice-irc/registration.yml")
           homeserver.copy_from_host(
-              pathlib.Path(os.environ.get("out", os.getcwd())) / "registration.yml", "/"
+              str(pathlib.Path(os.environ.get("out", os.getcwd())) / "registration.yml"), "/"
           )
           homeserver.succeed("chmod 444 /registration.yml")
 
@@ -209,7 +217,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
           )
 
           homeserver.wait_for_unit("matrix-synapse.service")
-          homeserver.wait_for_open_port(8448)
+          homeserver.wait_for_open_port(8008)
 
       with subtest("ensure messages can be exchanged"):
           client.succeed("do_test ${homeserverUrl} >&2")
diff --git a/nixos/tests/matrix-conduit.nix b/nixos/tests/matrix/conduit.nix
index d159fbaa48001..780837f962fa8 100644
--- a/nixos/tests/matrix-conduit.nix
+++ b/nixos/tests/matrix/conduit.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ../make-test-python.nix ({ pkgs, ... }:
   let
     name = "conduit";
   in
diff --git a/nixos/tests/dendrite.nix b/nixos/tests/matrix/dendrite.nix
index a444c9b200189..82e71d912130d 100644
--- a/nixos/tests/dendrite.nix
+++ b/nixos/tests/matrix/dendrite.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix (
+import ../make-test-python.nix (
   { pkgs, ... }:
     let
       homeserverUrl = "http://homeserver:8008";
@@ -17,9 +17,11 @@ import ./make-test-python.nix (
           homeserver = { pkgs, ... }: {
             services.dendrite = {
               enable = true;
+              loadCredential = [ "test_private_key:${private_key}" ];
+              openRegistration = true;
               settings = {
                 global.server_name = "test-dendrite-server.com";
-                global.private_key = private_key;
+                global.private_key = "$CREDENTIALS_DIRECTORY/test_private_key";
                 client_api.registration_disabled = false;
               };
             };
diff --git a/nixos/tests/matrix/mjolnir.nix b/nixos/tests/matrix/mjolnir.nix
index bb55f6f5440b2..cb843e2e9e3e8 100644
--- a/nixos/tests/matrix/mjolnir.nix
+++ b/nixos/tests/matrix/mjolnir.nix
@@ -38,26 +38,32 @@ import ../make-test-python.nix (
       homeserver = { pkgs, ... }: {
         services.matrix-synapse = {
           enable = true;
-          database_type = "sqlite3";
-          tls_certificate_path = "${cert}";
-          tls_private_key_path = "${key}";
-          enable_registration = true;
-          registration_shared_secret = "supersecret-registration";
-
-          listeners = [
-            # The default but tls=false
-            {
-              "bind_address" = "";
-              "port" = 8448;
-              "resources" = [
-                { "compress" = true; "names" = [ "client" "webclient" ]; }
-                { "compress" = false; "names" = [ "federation" ]; }
+          settings = {
+            database.name = "sqlite3";
+            tls_certificate_path = "${cert}";
+            tls_private_key_path = "${key}";
+            enable_registration = true;
+            enable_registration_without_verification = true;
+            registration_shared_secret = "supersecret-registration";
+
+            listeners = [ {
+              # The default but tls=false
+              bind_addresses = [
+                "0.0.0.0"
               ];
-              "tls" = false;
-              "type" = "http";
-              "x_forwarded" = false;
-            }
-          ];
+              port = 8448;
+              resources = [ {
+                compress = true;
+                names = [ "client" ];
+              } {
+                compress = false;
+                names = [ "federation" ];
+              } ];
+              tls = false;
+              type = "http";
+              x_forwarded = false;
+            } ];
+          };
         };
 
         networking.firewall.allowedTCPPorts = [ 8448 ];
diff --git a/nixos/tests/matrix/pantalaimon.nix b/nixos/tests/matrix/pantalaimon.nix
index fcb9904b21388..b5d649e6517aa 100644
--- a/nixos/tests/matrix/pantalaimon.nix
+++ b/nixos/tests/matrix/pantalaimon.nix
@@ -36,7 +36,7 @@ import ../make-test-python.nix (
       maintainers = teams.matrix.members;
     };
 
-    machine = { pkgs, ... }: {
+    nodes.machine = { pkgs, ... }: {
       services.pantalaimon-headless.instances.${pantalaimonInstanceName} = {
         homeserver = "https://localhost:8448";
         listenAddress = "0.0.0.0";
@@ -47,9 +47,32 @@ import ../make-test-python.nix (
 
       services.matrix-synapse = {
         enable = true;
-        database_type = "sqlite3";
-        tls_certificate_path = "${cert}";
-        tls_private_key_path = "${key}";
+        settings = {
+          listeners = [ {
+            port = 8448;
+            bind_addresses = [
+              "127.0.0.1"
+              "::1"
+            ];
+            type = "http";
+            tls = true;
+            x_forwarded = false;
+            resources = [ {
+              names = [
+                "client"
+              ];
+              compress = true;
+            } {
+              names = [
+                "federation"
+              ];
+              compress = false;
+            } ];
+          } ];
+          database.name = "sqlite3";
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+        };
       };
     };
 
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix/synapse.nix
index 21e8c24e47136..756a8d5de49ad 100644
--- a/nixos/tests/matrix-synapse.nix
+++ b/nixos/tests/matrix/synapse.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... } : let
+import ../make-test-python.nix ({ pkgs, ... } : let
 
 
   runWithOpenSSL = file: cmd: pkgs.runCommand file {
@@ -27,12 +27,35 @@ import ./make-test-python.nix ({ pkgs, ... } : let
   '';
 
 
-  mailerCerts = import ./common/acme/server/snakeoil-certs.nix;
+  mailerCerts = import ../common/acme/server/snakeoil-certs.nix;
   mailerDomain = mailerCerts.domain;
   registrationSharedSecret = "unsecure123";
   testUser = "alice";
   testPassword = "alicealice";
   testEmail = "alice@example.com";
+
+  listeners = [ {
+    port = 8448;
+    bind_addresses = [
+      "127.0.0.1"
+      "::1"
+    ];
+    type = "http";
+    tls = true;
+    x_forwarded = false;
+    resources = [ {
+      names = [
+        "client"
+      ];
+      compress = true;
+    } {
+      names = [
+        "federation"
+      ];
+      compress = false;
+    } ];
+  } ];
+
 in {
 
   name = "matrix-synapse";
@@ -48,22 +71,24 @@ in {
     {
       services.matrix-synapse = {
         enable = true;
-        database_type = "psycopg2";
-        tls_certificate_path = "${cert}";
-        tls_private_key_path = "${key}";
-        database_args = {
-          password = "synapse";
+        settings = {
+          inherit listeners;
+          database = {
+            name = "psycopg2";
+            args.password = "synapse";
+          };
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+          registration_shared_secret = registrationSharedSecret;
+          public_baseurl = "https://example.com";
+          email = {
+            smtp_host = mailerDomain;
+            smtp_port = 25;
+            require_transport_security = true;
+            notif_from = "matrix <matrix@${mailerDomain}>";
+            app_name = "Matrix";
+          };
         };
-        registration_shared_secret = registrationSharedSecret;
-        public_baseurl = "https://example.com";
-        extraConfig = ''
-          email:
-            smtp_host: "${mailerDomain}"
-            smtp_port: 25
-            require_transport_security: true
-            notif_from: "matrix <matrix@${mailerDomain}>"
-            app_name: "Matrix"
-        '';
       };
       services.postgresql = {
         enable = true;
@@ -165,9 +190,12 @@ in {
     serversqlite = args: {
       services.matrix-synapse = {
         enable = true;
-        database_type = "sqlite3";
-        tls_certificate_path = "${cert}";
-        tls_private_key_path = "${key}";
+        settings = {
+          inherit listeners;
+          database.name = "sqlite3";
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+        };
       };
     };
   };
diff --git a/nixos/tests/mediawiki.nix b/nixos/tests/mediawiki.nix
index 702fefefa1610..7f31d6aadfa2c 100644
--- a/nixos/tests/mediawiki.nix
+++ b/nixos/tests/mediawiki.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "mediawiki";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.mediawiki.enable = true;
       services.mediawiki.virtualHost.hostName = "localhost";
diff --git a/nixos/tests/meilisearch.nix b/nixos/tests/meilisearch.nix
index c379bda74c59a..6d7514a1cc0a8 100644
--- a/nixos/tests/meilisearch.nix
+++ b/nixos/tests/meilisearch.nix
@@ -5,14 +5,15 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     apiUrl = "http://${listenAddress}:${toString listenPort}";
     uid = "movies";
     indexJSON = pkgs.writeText "index.json" (builtins.toJSON { inherit uid; });
-    moviesJSON = pkgs.runCommand "movies.json" {} ''
-      sed -n '1,5p;$p' ${pkgs.meilisearch.src}/datasets/movies/movies.json > $out
-    '';
+    moviesJSON = pkgs.fetchurl {
+      url = "https://github.com/meilisearch/meilisearch/raw/v0.23.1/datasets/movies/movies.json";
+      sha256 = "1r3srld63dpmg9yrmysm6xl175661j5cspi93mk5q2wf8xwn50c5";
+    };
   in {
     name = "meilisearch";
     meta.maintainers = with lib.maintainers; [ Br1ght0ne ];
 
-    machine = { ... }: {
+    nodes.machine = { ... }: {
       environment.systemPackages = with pkgs; [ curl jq ];
       services.meilisearch = {
         enable = true;
@@ -26,7 +27,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       start_all()
 
       machine.wait_for_unit("meilisearch")
-      machine.wait_for_open_port("7700")
+      machine.wait_for_open_port(7700)
 
       with subtest("check version"):
           version = json.loads(machine.succeed("curl ${apiUrl}/version"))
@@ -34,7 +35,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
       with subtest("create index"):
           machine.succeed(
-              "curl -XPOST ${apiUrl}/indexes --data @${indexJSON}"
+              "curl -XPOST --header 'Content-Type: application/json' ${apiUrl}/indexes --data @${indexJSON}"
           )
           indexes = json.loads(machine.succeed("curl ${apiUrl}/indexes"))
           assert len(indexes) == 1, "index wasn't created"
@@ -42,7 +43,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       with subtest("add documents"):
           response = json.loads(
               machine.succeed(
-                  "curl -XPOST ${apiUrl}/indexes/${uid}/documents --data @${moviesJSON}"
+                  "curl -XPOST --header 'Content-Type: application/json' ${apiUrl}/indexes/${uid}/documents --data @${moviesJSON}"
               )
           )
           update_id = response["updateId"]
diff --git a/nixos/tests/memcached.nix b/nixos/tests/memcached.nix
index 31f5627d25ceb..6549995110d79 100644
--- a/nixos/tests/memcached.nix
+++ b/nixos/tests/memcached.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "memcached";
 
-  machine = {
+  nodes.machine = {
     imports = [ ../modules/profiles/minimal.nix ];
     services.memcached.enable = true;
   };
diff --git a/nixos/tests/mimir.nix b/nixos/tests/mimir.nix
new file mode 100644
index 0000000000000..f1b30d261472b
--- /dev/null
+++ b/nixos/tests/mimir.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "mimir";
+  nodes = {
+    server = { ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+      services.mimir.enable = true;
+      services.mimir.configuration = {
+        ingester.ring.replication_factor = 1;
+      };
+
+      services.telegraf.enable = true;
+      services.telegraf.extraConfig = {
+        agent.interval = "1s";
+        agent.flush_interval = "1s";
+        inputs.exec = {
+          commands = [
+            "${pkgs.coreutils}/bin/echo 'foo i=42i'"
+          ];
+          data_format = "influx";
+        };
+        outputs = {
+          http = {
+            # test remote write
+            url = "http://localhost:8080/api/v1/push";
+
+            # Data format to output.
+            data_format = "prometheusremotewrite";
+
+            headers = {
+              Content-Type = "application/x-protobuf";
+              Content-Encoding = "snappy";
+              X-Scope-OrgID = "nixos";
+              X-Prometheus-Remote-Write-Version = "0.1.0";
+            };
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("mimir.service")
+    server.wait_for_unit("telegraf.service")
+    server.wait_for_open_port(8080)
+    server.wait_until_succeeds(
+        "curl -H 'X-Scope-OrgID: nixos' http://127.0.0.1:8080/prometheus/api/v1/label/host/values | jq -r '.data[0]' | grep server"
+    )
+  '';
+})
diff --git a/nixos/tests/minecraft-server.nix b/nixos/tests/minecraft-server.nix
index dbe2cd6d56fe1..a51b36ff53084 100644
--- a/nixos/tests/minecraft-server.nix
+++ b/nixos/tests/minecraft-server.nix
@@ -33,5 +33,6 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
     assert "${seed}" in server.succeed(
         "mcrcon -H localhost -P ${toString rcon-port} -p '${rcon-pass}' -c 'seed'"
     )
+    server.succeed("systemctl stop minecraft-server")
   '';
 })
diff --git a/nixos/tests/minidlna.nix b/nixos/tests/minidlna.nix
index d852c7f60bc45..76039b0bb42c5 100644
--- a/nixos/tests/minidlna.nix
+++ b/nixos/tests/minidlna.nix
@@ -32,8 +32,10 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     start_all()
     server.succeed("mkdir -p /tmp/stuff && chown minidlna: /tmp/stuff")
     server.wait_for_unit("minidlna")
-    server.wait_for_open_port("8200")
-    server.succeed("curl --fail http://localhost:8200/")
-    client.succeed("curl --fail http://server:8200/")
+    server.wait_for_open_port(8200)
+    # requests must be made *by IP* to avoid triggering minidlna's
+    # DNS-rebinding protection
+    server.succeed("curl --fail http://$(getent ahostsv4 localhost | head -n1 | cut -f 1 -d ' '):8200/")
+    client.succeed("curl --fail http://$(getent ahostsv4 server | head -n1 | cut -f 1 -d ' '):8200/")
   '';
 })
diff --git a/nixos/tests/miniflux.nix b/nixos/tests/miniflux.nix
index 1015550fa8c71..d905aea048a3a 100644
--- a/nixos/tests/miniflux.nix
+++ b/nixos/tests/miniflux.nix
@@ -7,6 +7,15 @@ let
   defaultPort = 8080;
   defaultUsername = "admin";
   defaultPassword = "password";
+  adminCredentialsFile = pkgs.writeText "admin-credentials" ''
+            ADMIN_USERNAME=${defaultUsername}
+            ADMIN_PASSWORD=${defaultPassword}
+          '';
+  customAdminCredentialsFile = pkgs.writeText "admin-credentials" ''
+            ADMIN_USERNAME=${username}
+            ADMIN_PASSWORD=${password}
+          '';
+
 in
 with lib;
 {
@@ -17,13 +26,19 @@ with lib;
     default =
       { ... }:
       {
-        services.miniflux.enable = true;
+        services.miniflux = {
+          enable = true;
+          inherit adminCredentialsFile;
+        };
       };
 
     withoutSudo =
       { ... }:
       {
-        services.miniflux.enable = true;
+        services.miniflux = {
+          enable = true;
+          inherit adminCredentialsFile;
+        };
         security.sudo.enable = false;
       };
 
@@ -36,10 +51,7 @@ with lib;
             CLEANUP_FREQUENCY = "48";
             LISTEN_ADDR = "localhost:${toString port}";
           };
-          adminCredentialsFile = pkgs.writeText "admin-credentials" ''
-            ADMIN_USERNAME=${username}
-            ADMIN_PASSWORD=${password}
-          '';
+          adminCredentialsFile = customAdminCredentialsFile;
         };
       };
   };
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index 0587912c9a226..0d5f0fe2f044c 100644
--- a/nixos/tests/misc.nix
+++ b/nixos/tests/misc.nix
@@ -1,14 +1,14 @@
 # Miscellaneous small tests that don't warrant their own VM run.
 
-import ./make-test-python.nix ({ pkgs, ...} : rec {
+import ./make-test-python.nix ({ pkgs, ...} : let
+  foo = pkgs.writeText "foo" "Hello World";
+in {
   name = "misc";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
 
-  foo = pkgs.writeText "foo" "Hello World";
-
-  machine =
+  nodes.machine =
     { lib, ... }:
     with lib;
     { swapDevices = mkOverride 0
diff --git a/nixos/tests/mod_perl.nix b/nixos/tests/mod_perl.nix
index 29a1eb6503fdf..f29d79ea6206a 100644
--- a/nixos/tests/mod_perl.nix
+++ b/nixos/tests/mod_perl.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = [ sgo ];
   };
 
-  machine = { config, lib, pkgs, ... }: {
+  nodes.machine = { config, lib, pkgs, ... }: {
     services.httpd = {
       enable = true;
       adminAddr = "admin@localhost";
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
index 9c6fdfb1ca76e..edd074f5163c2 100644
--- a/nixos/tests/mongodb.nix
+++ b/nixos/tests/mongodb.nix
@@ -37,6 +37,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
           mongodb-3_6
           mongodb-4_0
           mongodb-4_2
+          mongodb-4_4
+          mongodb-5_0
         ];
       };
     };
@@ -48,6 +50,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
       + runMongoDBTest pkgs.mongodb-3_6
       + runMongoDBTest pkgs.mongodb-4_0
       + runMongoDBTest pkgs.mongodb-4_2
+      + runMongoDBTest pkgs.mongodb-4_4
+      + runMongoDBTest pkgs.mongodb-5_0
       + ''
         node.shutdown()
       '';
diff --git a/nixos/tests/moodle.nix b/nixos/tests/moodle.nix
index 56aa62596c078..4570e89638822 100644
--- a/nixos/tests/moodle.nix
+++ b/nixos/tests/moodle.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "moodle";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.moodle.enable = true;
       services.moodle.virtualHost.hostName = "localhost";
diff --git a/nixos/tests/moonraker.nix b/nixos/tests/moonraker.nix
new file mode 100644
index 0000000000000..b0a93a4a608b4
--- /dev/null
+++ b/nixos/tests/moonraker.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "moonraker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ zhaofengli ];
+  };
+
+  nodes = {
+    printer = { config, pkgs, ... }: {
+      security.polkit.enable = true;
+
+      services.moonraker = {
+        enable = true;
+        allowSystemControl = true;
+
+        settings = {
+          authorization = {
+            trusted_clients = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+      };
+
+      services.klipper = {
+        enable = true;
+
+        user = "moonraker";
+        group = "moonraker";
+
+        # No mcu configured so won't even enter `ready` state
+        settings = {};
+      };
+    };
+  };
+
+  testScript = ''
+    printer.start()
+
+    printer.wait_for_unit("klipper.service")
+    printer.wait_for_unit("moonraker.service")
+    printer.wait_until_succeeds("curl http://localhost:7125/printer/info | grep -v 'Not Found' >&2", timeout=30)
+
+    with subtest("Check that we can perform system-level operations"):
+        printer.succeed("curl -X POST http://localhost:7125/machine/services/stop?service=klipper | grep ok >&2")
+        printer.wait_until_succeeds("systemctl --no-pager show klipper.service | grep ActiveState=inactive", timeout=10)
+  '';
+})
diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix
index 36cc8e3e3d9bd..d516d3373d9f6 100644
--- a/nixos/tests/mosquitto.nix
+++ b/nixos/tests/mosquitto.nix
@@ -4,6 +4,7 @@ let
   port = 1888;
   tlsPort = 1889;
   anonPort = 1890;
+  bindTestPort = 1891;
   password = "VERY_secret";
   hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw==";
   topic = "test/foo";
@@ -125,6 +126,10 @@ in {
               };
             };
           }
+          {
+            settings.bind_interface = "eth0";
+            port = bindTestPort;
+          }
         ];
       };
     };
@@ -134,6 +139,8 @@ in {
   };
 
   testScript = ''
+    import json
+
     def mosquitto_cmd(binary, user, topic, port):
         return (
             "mosquitto_{} "
@@ -162,6 +169,27 @@ in {
     start_all()
     server.wait_for_unit("mosquitto.service")
 
+    with subtest("bind_interface"):
+        addrs = dict()
+        for iface in json.loads(server.succeed("ip -json address show")):
+            for addr in iface['addr_info']:
+                # don't want to deal with multihoming here
+                assert addr['local'] not in addrs
+                addrs[addr['local']] = (iface['ifname'], addr['family'])
+
+        # mosquitto grabs *one* random address per type for bind_interface
+        (has4, has6) = (False, False)
+        for line in server.succeed("ss -HlptnO sport = ${toString bindTestPort}").splitlines():
+            items = line.split()
+            if "mosquitto" not in items[5]: continue
+            listener = items[3].rsplit(':', maxsplit=1)[0].strip('[]')
+            assert listener in addrs
+            assert addrs[listener][0] == "eth0"
+            has4 |= addrs[listener][1] == 'inet'
+            has6 |= addrs[listener][1] == 'inet6'
+        assert has4
+        assert has6
+
     with subtest("check passwords"):
         client1.succeed(publish("-m test", "password_store"))
         client1.succeed(publish("-m test", "password_file"))
diff --git a/nixos/tests/mtp.nix b/nixos/tests/mtp.nix
new file mode 100644
index 0000000000000..8f0835d75d3fe
--- /dev/null
+++ b/nixos/tests/mtp.nix
@@ -0,0 +1,109 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "mtp";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ matthewcroughan nixinator ];
+  };
+
+  nodes =
+  {
+    client = { config, pkgs, ... }: {
+      # DBUS runs only once a user session is created, which means a user has to
+      # login. Here, we log in as root. Once logged in, the gvfs-daemon service runs
+      # as UID 0 in User-0.service
+      services.getty.autologinUser = "root";
+
+      # XDG_RUNTIME_DIR is needed for running systemd-user services such as
+      # gvfs-daemon as root.
+      environment.variables.XDG_RUNTIME_DIR = "/run/user/0";
+
+      environment.systemPackages = with pkgs; [ usbutils glib jmtpfs tree ];
+      services.gvfs.enable = true;
+
+      # Creates a usb-mtp device inside the VM, which is mapped to the host's
+      # /tmp folder, it is able to write files to this location, but only has
+      # permissions to read its own creations.
+      virtualisation.qemu.options = [
+        "-usb"
+        "-device usb-mtp,rootdir=/tmp,readonly=false"
+      ];
+    };
+  };
+
+
+  testScript = { nodes, ... }:
+    let
+      # Creates a list of QEMU MTP devices matching USB ID (46f4:0004). This
+      # value can be sourced in a shell script. This is so we can loop over the
+      # devices we find, as this test may want to use more than one MTP device
+      # in future.
+      mtpDevices = pkgs.writeScript "mtpDevices.sh" ''
+        export mtpDevices=$(lsusb -d 46f4:0004 | awk {'print $2","$4'} | sed 's/[:-]/ /g')
+      '';
+      # Qemu is only capable of creating an MTP device with Picture Transfer
+      # Protocol. This means that gvfs must use gphoto2:// rather than mtp://
+      # when mounting.
+      # https://github.com/qemu/qemu/blob/970bc16f60937bcfd334f14c614bd4407c247961/hw/usb/dev-mtp.c#L278
+      gvfs = rec {
+        mountAllMtpDevices = pkgs.writeScript "mountAllMtpDevices.sh" ''
+          set -e
+          source ${mtpDevices}
+          for i in $mtpDevices
+          do
+            gio mount "gphoto2://[usb:$i]/"
+          done
+        '';
+        unmountAllMtpDevices = pkgs.writeScript "unmountAllMtpDevices.sh" ''
+          set -e
+          source ${mtpDevices}
+          for i in $mtpDevices
+          do
+            gio mount -u "gphoto2://[usb:$i]/"
+          done
+        '';
+        # gvfsTest:
+        # 1. Creates a 10M test file
+        # 2. Copies it to the device using GIO tools
+        # 3. Checks for corruption with `diff`
+        # 4. Removes the file, then unmounts the disks.
+        gvfsTest = pkgs.writeScript "gvfsTest.sh" ''
+          set -e
+          source ${mtpDevices}
+          ${mountAllMtpDevices}
+          dd if=/dev/urandom of=testFile10M bs=1M count=10
+          for i in $mtpDevices
+          do
+            gio copy ./testFile10M gphoto2://[usb:$i]/
+            ls -lah /run/user/0/gvfs/*/testFile10M
+            gio remove gphoto2://[usb:$i]/testFile10M
+          done
+          ${unmountAllMtpDevices}
+        '';
+      };
+      jmtpfs = {
+        # jmtpfsTest:
+        # 1. Mounts the device on a dir named `phone` using jmtpfs
+        # 2. Puts the current Nixpkgs libmtp version into a file
+        # 3. Checks for corruption with `diff`
+        # 4. Prints the directory tree
+        jmtpfsTest = pkgs.writeScript "jmtpfsTest.sh" ''
+          set -e
+          mkdir phone
+          jmtpfs phone
+          echo "${pkgs.libmtp.version}" > phone/tmp/testFile
+          echo "${pkgs.libmtp.version}" > testFile
+          diff phone/tmp/testFile testFile
+          tree phone
+        '';
+      };
+    in
+    # Using >&2 allows the results of the scripts to be printed to the terminal
+    # when building this test with Nix. Scripts would otherwise complete
+    # silently.
+    ''
+    start_all()
+    client.wait_for_unit("multi-user.target")
+    client.wait_for_unit("dbus.service")
+    client.succeed("${gvfs.gvfsTest} >&2")
+    client.succeed("${jmtpfs.jmtpfsTest} >&2")
+  '';
+})
diff --git a/nixos/tests/musescore.nix b/nixos/tests/musescore.nix
index 7fd80d70df124..18de0a5502390 100644
--- a/nixos/tests/musescore.nix
+++ b/nixos/tests/musescore.nix
@@ -17,7 +17,7 @@ in
     maintainers = [ turion ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [
diff --git a/nixos/tests/mysql/mysql-autobackup.nix b/nixos/tests/mysql/mysql-autobackup.nix
index 101122f7bdef8..b49466db0a9ce 100644
--- a/nixos/tests/mysql/mysql-autobackup.nix
+++ b/nixos/tests/mysql/mysql-autobackup.nix
@@ -17,7 +17,7 @@ let
     name = "${name}-automysqlbackup";
     meta.maintainers = [ lib.maintainers.aanderse ];
 
-    machine = {
+    nodes.machine = {
       services.mysql = {
         inherit package;
         enable = true;
diff --git a/nixos/tests/mysql/mysql-backup.nix b/nixos/tests/mysql/mysql-backup.nix
index 9335b233327a7..968f56dd3c9bd 100644
--- a/nixos/tests/mysql/mysql-backup.nix
+++ b/nixos/tests/mysql/mysql-backup.nix
@@ -51,7 +51,6 @@ let
 
       # Do a backup and wait for it to start
       master.start_job("mysql-backup.service")
-      master.wait_for_unit("mysql-backup.service")
 
       # wait for backup to fail, because of database 'doesnotexist'
       master.wait_until_fails("systemctl is-active -q mysql-backup.service")
diff --git a/nixos/tests/n8n.nix b/nixos/tests/n8n.nix
index ed93639f2a429..c1753a418f673 100644
--- a/nixos/tests/n8n.nix
+++ b/nixos/tests/n8n.nix
@@ -7,7 +7,7 @@ let
 in
 {
   name = "n8n";
-  meta.maintainers = with maintainers; [ freezeboy ];
+  meta.maintainers = with maintainers; [ freezeboy k900 ];
 
   nodes.machine =
     { pkgs, ... }:
@@ -19,7 +19,7 @@ in
 
   testScript = ''
     machine.wait_for_unit("n8n.service")
-    machine.wait_for_open_port("${toString port}")
+    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 e4d8dabedf72c..b6e45fc103afd 100644
--- a/nixos/tests/nagios.nix
+++ b/nixos/tests/nagios.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix (
       maintainers = [ symphorien ];
     };
 
-    machine = { lib, ... }: let
+    nodes.machine = { lib, ... }: let
       writer = pkgs.writeShellScript "write" ''
         set -x
         echo "$@"  >> /tmp/notifications
diff --git a/nixos/tests/nano.nix b/nixos/tests/nano.nix
deleted file mode 100644
index 6585a6842e857..0000000000000
--- a/nixos/tests/nano.nix
+++ /dev/null
@@ -1,44 +0,0 @@
-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
index 9ee738ffb170e..bb95ccb36911d 100644
--- a/nixos/tests/nar-serve.nix
+++ b/nixos/tests/nar-serve.nix
@@ -31,7 +31,7 @@ import ./make-test-python.nix (
 
       # Create a fake cache with Nginx service the static files
       server.succeed(
-          "nix copy --to file:///var/www ${pkgs.hello}"
+          "nix --experimental-features nix-command copy --to file:///var/www ${pkgs.hello}"
       )
       server.wait_for_unit("nginx.service")
       server.wait_for_open_port(80)
diff --git a/nixos/tests/nats.nix b/nixos/tests/nats.nix
index bee36f262f4cf..c650904e53bf7 100644
--- a/nixos/tests/nats.nix
+++ b/nixos/tests/nats.nix
@@ -45,21 +45,19 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }: {
             "{}"
         ).format(" ".join(args))
 
+    def parallel(*fns):
+        from threading import Thread
+        threads = [ Thread(target=fn) for fn in fns ]
+        for t in threads: t.start()
+        for t in threads: t.join()
+
     start_all()
     server.wait_for_unit("nats.service")
 
-    client1.fail("test -f ${file}")
-
-    # Subscribe on topic on client1 and echo messages to file.
-    client1.execute("({} | tee ${file} &)".format(nats_cmd("sub", "--raw", "${topic}")))
-
-    # Give client1 some time to subscribe.
-    client1.execute("sleep 2")
-
-    # Publish message on client2.
-    client2.execute(nats_cmd("pub", "${topic}", "hello"))
-
-    # Check if message has been received.
-    client1.succeed("grep -q hello ${file}")
+    with subtest("pub sub"):
+        parallel(
+            lambda: client1.succeed(nats_cmd("sub", "--count", "1", "${topic}")),
+            lambda: client2.succeed("sleep 2 && {}".format(nats_cmd("pub", "${topic}", "hello"))),
+        )
   '';
 })
diff --git a/nixos/tests/navidrome.nix b/nixos/tests/navidrome.nix
index 42e14720b2edf..7315aef624017 100644
--- a/nixos/tests/navidrome.nix
+++ b/nixos/tests/navidrome.nix
@@ -1,12 +1,12 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "navidrome";
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.navidrome.enable = true;
   };
 
   testScript = ''
     machine.wait_for_unit("navidrome")
-    machine.wait_for_open_port("4533")
+    machine.wait_for_open_port(4533)
   '';
 })
diff --git a/nixos/tests/nbd.nix b/nixos/tests/nbd.nix
new file mode 100644
index 0000000000000..b4aaf29ee4e50
--- /dev/null
+++ b/nixos/tests/nbd.nix
@@ -0,0 +1,103 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    listenPort = 30123;
+    testString = "It works!";
+    mkCreateSmallFileService = { path, loop ? false }: {
+      script = ''
+        ${pkgs.coreutils}/bin/dd if=/dev/zero of=${path} bs=1K count=100
+        ${pkgs.lib.optionalString loop
+          "${pkgs.util-linux}/bin/losetup --find ${path}"}
+      '';
+      serviceConfig = {
+        Type = "oneshot";
+      };
+      wantedBy = [ "multi-user.target" ];
+      before = [ "nbd-server.service" ];
+    };
+  in
+  {
+    name = "nbd";
+
+    nodes = {
+      server = { config, pkgs, ... }: {
+        # Create some small files of zeros to use as the ndb disks
+        ## `vault-pub.disk` is accessible from any IP
+        systemd.services.create-pub-file =
+          mkCreateSmallFileService { path = "/vault-pub.disk"; };
+        ## `vault-priv.disk` is accessible only from localhost.
+        ## It's also a loopback device to test exporting /dev/...
+        systemd.services.create-priv-file =
+          mkCreateSmallFileService { path = "/vault-priv.disk"; loop = true; };
+        ## `aaa.disk` is just here because "[aaa]" sorts before
+        ## "[generic]" lexicographically, and nbd-server breaks if
+        ## "[generic]" isn't the first section.
+        systemd.services.create-aaa-file =
+          mkCreateSmallFileService { path = "/aaa.disk"; };
+
+        # Needed only for nbd-client used in the tests.
+        environment.systemPackages = [ pkgs.nbd ];
+
+        # Open the nbd port in the firewall
+        networking.firewall.allowedTCPPorts = [ listenPort ];
+
+        # Run the nbd server and expose the small file created above
+        services.nbd.server = {
+          enable = true;
+          exports = {
+            aaa = {
+              path = "/aaa.disk";
+            };
+            vault-pub = {
+              path = "/vault-pub.disk";
+            };
+            vault-priv = {
+              path = "/dev/loop0";
+              allowAddresses = [ "127.0.0.1" "::1" ];
+            };
+          };
+          listenAddress = "0.0.0.0";
+          listenPort = listenPort;
+        };
+      };
+
+      client = { config, pkgs, ... }: {
+        programs.nbd.enable = true;
+      };
+    };
+
+    testScript = ''
+      testString = "${testString}"
+
+      start_all()
+      server.wait_for_open_port(${toString listenPort})
+
+      # Client: Connect to the server, write a small string to the nbd disk, and cleanly disconnect
+      client.succeed("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-pub -persist")
+      client.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+      client.succeed("nbd-client -d /dev/nbd0")
+
+      # Server: Check that the string written by the client is indeed in the file
+      foundString = server.succeed(f"dd status=none if=/vault-pub.disk count={len(testString)}")[:len(testString)]
+      if foundString != testString:
+         raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+
+      # Client: Fail to connect to the private disk
+      client.fail("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+
+      # Server: Successfully connect to the private disk
+      server.succeed("nbd-client localhost ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+      server.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+      foundString = server.succeed(f"dd status=none if=/dev/loop0 count={len(testString)}")[:len(testString)]
+      if foundString != testString:
+         raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+      server.succeed("nbd-client -d /dev/nbd0")
+
+      # Server: Successfully connect to the aaa disk
+      server.succeed("nbd-client localhost ${toString listenPort} /dev/nbd0 -name aaa -persist")
+      server.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+      foundString = server.succeed(f"dd status=none if=/aaa.disk count={len(testString)}")[:len(testString)]
+      if foundString != testString:
+         raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+      server.succeed("nbd-client -d /dev/nbd0")
+    '';
+  })
diff --git a/nixos/tests/ncdns.nix b/nixos/tests/ncdns.nix
index 50193676f34f3..3ce39ed3cb55c 100644
--- a/nixos/tests/ncdns.nix
+++ b/nixos/tests/ncdns.nix
@@ -29,10 +29,10 @@ in
   };
 
   nodes.server = { ... }: {
-    networking.nameservers = [ "127.0.0.1" ];
+    networking.nameservers = [ "::1" ];
 
     services.namecoind.rpc = {
-      address = "127.0.0.1";
+      address = "::1";
       user = "namecoin";
       password = "secret";
       port = 8332;
@@ -45,7 +45,7 @@ in
       script = ''
         while true; do
           echo -e "HTTP/1.1 200 OK\n\n $(<${fakeReply})\n" \
-            | ${pkgs.netcat}/bin/nc -N -l 127.0.0.1 8332
+            | ${pkgs.netcat}/bin/nc -N -l ::1 8332
         done
       '';
     };
@@ -58,14 +58,10 @@ in
       identity.address    = "1.0.0.1";
     };
 
-    services.pdns-recursor = {
-      enable = true;
-      dns.allowFrom = [ "127.0.0.0/8" ];
-      resolveNamecoin = true;
-    };
+    services.pdns-recursor.enable = true;
+    services.pdns-recursor.resolveNamecoin = true;
 
     environment.systemPackages = [ pkgs.dnsutils ];
-
   };
 
   testScript =
@@ -77,20 +73,21 @@ in
 
       with subtest("DNSKEY bit record is present"):
           server.wait_for_unit("pdns-recursor")
-          server.wait_for_open_port("53")
+          server.wait_for_open_port(53)
           server.succeed("host -t DNSKEY bit")
     '') +
     ''
       with subtest("can resolve a .bit name"):
           server.wait_for_unit("namecoind")
           server.wait_for_unit("ncdns")
-          server.wait_for_open_port("8332")
+          server.wait_for_open_port(8332)
           assert "1.2.3.4" in server.succeed("dig @localhost -p 5333 test.bit")
 
       with subtest("SOA record has identity information"):
           assert "example.com" in server.succeed("dig SOA @localhost -p 5333 bit")
 
       with subtest("bit. zone forwarding works"):
+          server.wait_for_unit("pdns-recursor")
           assert "1.2.3.4" in server.succeed("host test.bit")
     '';
 })
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 2510937b5dcd7..71b82b871270e 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -77,12 +77,14 @@ let
   testCases = {
     loopback = {
       name = "Loopback";
-      machine.networking.useDHCP = false;
-      machine.networking.useNetworkd = networkd;
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        networking.useDHCP = false;
+        networking.useNetworkd = networkd;
+      };
       testScript = ''
         start_all()
-        machine.wait_for_unit("network.target")
-        loopback_addresses = machine.succeed("ip addr show lo")
+        client.wait_for_unit("network.target")
+        loopback_addresses = client.succeed("ip addr show lo")
         assert "inet 127.0.0.1/8" in loopback_addresses
         assert "inet6 ::1/128" in loopback_addresses
       '';
@@ -96,6 +98,7 @@ let
           useNetworkd = networkd;
           useDHCP = false;
           defaultGateway = "192.168.1.1";
+          defaultGateway6 = "fd00:1234:5678:1::1";
           interfaces.eth1.ipv4.addresses = mkOverride 0 [
             { address = "192.168.1.2"; prefixLength = 24; }
             { address = "192.168.1.3"; prefixLength = 32; }
@@ -137,8 +140,49 @@ let
           with subtest("Test default gateway"):
               router.wait_until_succeeds("ping -c 1 192.168.3.1")
               client.wait_until_succeeds("ping -c 1 192.168.3.1")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:3::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:3::1")
         '';
     };
+    routeType = {
+      name = "RouteType";
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        networking = {
+          useDHCP = false;
+          useNetworkd = networkd;
+          interfaces.eth1.ipv4.routes = [{
+            address = "192.168.1.127";
+            prefixLength = 32;
+            type = "local";
+          }];
+        };
+      };
+      testScript = ''
+        start_all()
+        client.wait_for_unit("network.target")
+        client.succeed("ip -4 route list table local | grep 'local 192.168.1.127'")
+      '';
+    };
+    dhcpDefault = {
+      name = "useDHCP-by-default";
+      nodes.router = router;
+      nodes.client = { lib, ... }: {
+        # Disable test driver default config
+        networking.interfaces = lib.mkForce {};
+        networking.useNetworkd = networkd;
+        virtualisation.vlans = [ 1 ];
+      };
+      testScript = ''
+        start_all()
+        client.wait_for_unit("multi-user.target")
+        client.wait_until_succeeds("ip addr show dev eth1 | grep '192.168.1'")
+        client.shell_interact()
+        client.succeed("ping -c 1 192.168.1.1")
+        router.succeed("ping -c 1 192.168.1.1")
+        router.succeed("ping -c 1 192.168.1.2")
+        client.succeed("ping -c 1 192.168.1.2")
+      '';
+    };
     dhcpSimple = {
       name = "SimpleDHCP";
       nodes.router = router;
@@ -246,13 +290,13 @@ let
         networking = {
           useNetworkd = networkd;
           useDHCP = false;
-          bonds.bond = {
+          bonds.bond0 = {
             interfaces = [ "eth1" "eth2" ];
-            driverOptions.mode = "balance-rr";
+            driverOptions.mode = "802.3ad";
           };
           interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
           interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
-          interfaces.bond.ipv4.addresses = mkOverride 0
+          interfaces.bond0.ipv4.addresses = mkOverride 0
             [ { inherit address; prefixLength = 30; } ];
         };
       };
@@ -274,6 +318,10 @@ let
 
               client2.wait_until_succeeds("ping -c 2 192.168.1.1")
               client2.wait_until_succeeds("ping -c 2 192.168.1.2")
+
+          with subtest("Verify bonding mode"):
+              for client in client1, client2:
+                  client.succeed('grep -q "Bonding Mode: IEEE 802.3ad Dynamic link aggregation" /proc/net/bonding/bond0')
         '';
     };
     bridge = let
@@ -494,6 +542,7 @@ let
         networking = {
           useNetworkd = networkd;
           useDHCP = false;
+          firewall.extraCommands = "ip6tables -A nixos-fw -p gre -j nixos-fw-accept";
         };
       };
     in {
@@ -502,21 +551,35 @@ let
         mkMerge [
           (node args)
           {
-            virtualisation.vlans = [ 1 2 ];
+            virtualisation.vlans = [ 1 2 4 ];
             networking = {
               greTunnels = {
                 greTunnel = {
                   local = "192.168.2.1";
                   remote = "192.168.2.2";
                   dev = "eth2";
+                  ttl = 225;
                   type = "tap";
                 };
+                gre6Tunnel = {
+                  local = "fd00:1234:5678:4::1";
+                  remote = "fd00:1234:5678:4::2";
+                  dev = "eth3";
+                  ttl = 255;
+                  type = "tun6";
+                };
               };
               bridges.bridge.interfaces = [ "greTunnel" "eth1" ];
               interfaces.eth1.ipv4.addresses = mkOverride 0 [];
               interfaces.bridge.ipv4.addresses = mkOverride 0 [
                 { address = "192.168.1.1"; prefixLength = 24; }
               ];
+              interfaces.eth3.ipv6.addresses = [
+                { address = "fd00:1234:5678:4::1"; prefixLength = 64; }
+              ];
+              interfaces.gre6Tunnel.ipv6.addresses = mkOverride 0 [
+                { address = "fc00::1"; prefixLength = 64; }
+              ];
             };
           }
         ];
@@ -524,26 +587,41 @@ let
         mkMerge [
           (node args)
           {
-            virtualisation.vlans = [ 2 3 ];
+            virtualisation.vlans = [ 2 3 4 ];
             networking = {
               greTunnels = {
                 greTunnel = {
                   local = "192.168.2.2";
                   remote = "192.168.2.1";
                   dev = "eth1";
+                  ttl = 225;
                   type = "tap";
                 };
+                gre6Tunnel = {
+                  local = "fd00:1234:5678:4::2";
+                  remote = "fd00:1234:5678:4::1";
+                  dev = "eth3";
+                  ttl = 255;
+                  type = "tun6";
+                };
               };
               bridges.bridge.interfaces = [ "greTunnel" "eth2" ];
               interfaces.eth2.ipv4.addresses = mkOverride 0 [];
               interfaces.bridge.ipv4.addresses = mkOverride 0 [
                 { address = "192.168.1.2"; prefixLength = 24; }
               ];
+              interfaces.eth3.ipv6.addresses = [
+                { address = "fd00:1234:5678:4::2"; prefixLength = 64; }
+              ];
+              interfaces.gre6Tunnel.ipv6.addresses = mkOverride 0 [
+                { address = "fc00::2"; prefixLength = 64; }
+              ];
             };
           }
         ];
       testScript = { ... }:
         ''
+          import json
           start_all()
 
           with subtest("Wait for networking to be configured"):
@@ -558,6 +636,17 @@ let
               client1.wait_until_succeeds("ping -c 1 192.168.1.2")
 
               client2.wait_until_succeeds("ping -c 1 192.168.1.1")
+
+              client1.wait_until_succeeds("ping -c 1 fc00::2")
+
+              client2.wait_until_succeeds("ping -c 1 fc00::1")
+
+          with subtest("Test GRE tunnel TTL"):
+              links = json.loads(client1.succeed("ip -details -json link show greTunnel"))
+              assert links[0]['linkinfo']['info_data']['ttl'] == 225, "ttl not set for greTunnel"
+
+              links = json.loads(client2.succeed("ip -details -json link show gre6Tunnel"))
+              assert links[0]['linkinfo']['info_data']['ttl'] == 255, "ttl not set for gre6Tunnel"
         '';
     };
     vlan = let
@@ -593,9 +682,49 @@ let
               client2.succeed("ip addr show dev vlan >&2")
         '';
     };
+    vlan-ping = let
+        baseIP = number: "10.10.10.${number}";
+        vlanIP = number: "10.1.1.${number}";
+        baseInterface = "eth1";
+        vlanInterface = "vlan42";
+        node = number: {pkgs, ... }: with pkgs.lib; {
+          virtualisation.vlans = [ 1 ];
+          networking = {
+            #useNetworkd = networkd;
+            useDHCP = false;
+            vlans.${vlanInterface} = { id = 42; interface = baseInterface; };
+            interfaces.${baseInterface}.ipv4.addresses = mkOverride 0 [{ address = baseIP number; prefixLength = 24; }];
+            interfaces.${vlanInterface}.ipv4.addresses = mkOverride 0 [{ address = vlanIP number; prefixLength = 24; }];
+          };
+        };
+
+        serverNodeNum = "1";
+        clientNodeNum = "2";
+
+    in {
+      name = "vlan-ping";
+      nodes.server = node serverNodeNum;
+      nodes.client = node clientNodeNum;
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to be configured"):
+              server.wait_for_unit("network.target")
+              client.wait_for_unit("network.target")
+
+          with subtest("Test ping on base interface in setup"):
+              client.succeed("ping -I ${baseInterface} -c 1 ${baseIP serverNodeNum}")
+              server.succeed("ping -I ${baseInterface} -c 1 ${baseIP clientNodeNum}")
+
+          with subtest("Test ping on vlan subinterface in setup"):
+              client.succeed("ping -I ${vlanInterface} -c 1 ${vlanIP serverNodeNum}")
+              server.succeed("ping -I ${vlanInterface} -c 1 ${vlanIP clientNodeNum}")
+        '';
+    };
     virtual = {
       name = "Virtual";
-      machine = {
+      nodes.machine = {
         networking.useNetworkd = networkd;
         networking.useDHCP = false;
         networking.interfaces.tap0 = {
@@ -739,7 +868,7 @@ let
     };
     routes = {
       name = "routes";
-      machine = {
+      nodes.machine = {
         networking.useNetworkd = networkd;
         networking.useDHCP = false;
         networking.interfaces.eth0 = {
@@ -820,7 +949,7 @@ let
     };
     rename = {
       name = "RenameInterface";
-      machine = { pkgs, ... }: {
+      nodes.machine = { pkgs, ... }: {
         virtualisation.vlans = [ 1 ];
         networking = {
           useNetworkd = networkd;
@@ -833,7 +962,7 @@ let
                 linkConfig.Name = "custom_name";
               };
             }
-       else { services.udev.initrdRules = ''
+       else { boot.initrd.services.udev.rules = ''
                SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="custom_name"
               '';
             });
@@ -864,14 +993,14 @@ let
         print(client.succeed("ip l add name foo type dummy"))
         print(client.succeed("stat /etc/systemd/network/50-foo.link"))
         client.succeed("udevadm settle")
-        assert "mtu 1442" in client.succeed("ip l show dummy0")
+        assert "mtu 1442" in client.succeed("ip l show dev foo")
       '';
     };
     wlanInterface = let
       testMac = "06:00:00:00:02:00";
     in {
       name = "WlanInterface";
-      machine = { pkgs, ... }: {
+      nodes.machine = { pkgs, ... }: {
         boot.kernelModules = [ "mac80211_hwsim" ];
         networking.wlanInterfaces = {
           wlan0 = { device = "wlan0"; };
diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix
index 34d3c345354c7..9e378fe6a52d3 100644
--- a/nixos/tests/nextcloud/default.nix
+++ b/nixos/tests/nextcloud/default.nix
@@ -16,6 +16,10 @@ foldl
       inherit system pkgs;
       nextcloudVersion = ver;
     };
+    "with-declarative-redis-and-secrets${toString ver}" = import ./with-declarative-redis-and-secrets.nix {
+      inherit system pkgs;
+      nextcloudVersion = ver;
+    };
   })
 { }
-  [ 21 22 23 ]
+  [ 23 24 ]
diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
new file mode 100644
index 0000000000000..fda05bacb4fe4
--- /dev/null
+++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
@@ -0,0 +1,118 @@
+import ../make-test-python.nix ({ pkgs, ...}: let
+  adminpass = "hunter2";
+  adminuser = "custom-admin-username";
+in {
+  name = "nextcloud-with-declarative-redis";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eqyiel ];
+  };
+
+  nodes = {
+    # The only thing the client needs to do is download a file.
+    client = { ... }: {};
+
+    nextcloud = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 80 ];
+
+      services.nextcloud = {
+        enable = true;
+        hostName = "nextcloud";
+        caching = {
+          apcu = false;
+          redis = true;
+          memcached = false;
+        };
+        config = {
+          dbtype = "pgsql";
+          dbname = "nextcloud";
+          dbuser = "nextcloud";
+          dbhost = "/run/postgresql";
+          inherit adminuser;
+          adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
+            ${adminpass}
+          '');
+        };
+        secretFile = "/etc/nextcloud-secrets.json";
+
+        extraOptions.redis = {
+          host = "/run/redis/redis.sock";
+          port = 0;
+          dbindex = 0;
+          timeout = 1.5;
+          # password handled via secretfile below
+        };
+        extraOptions.memcache = {
+          local = "\OC\Memcache\Redis";
+          locking = "\OC\Memcache\Redis";
+        };
+      };
+
+      services.redis = {
+        enable = true;
+      };
+
+      systemd.services.nextcloud-setup= {
+        requires = ["postgresql.service"];
+        after = [
+          "postgresql.service"
+        ];
+      };
+
+      services.postgresql = {
+        enable = true;
+        ensureDatabases = [ "nextcloud" ];
+        ensureUsers = [
+          { name = "nextcloud";
+            ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
+          }
+        ];
+      };
+
+      # This file is meant to contain secret options which should
+      # not go into the nix store. Here it is just used to set the
+      # databyse type to postgres.
+      environment.etc."nextcloud-secrets.json".text = ''
+        {
+          "redis": {
+            "password": "secret"
+          }
+        }
+      '';
+    };
+  };
+
+  testScript = let
+    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
+      #!${pkgs.runtimeShell}
+      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
+      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/webdav/"
+      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
+      "''${@}"
+    '';
+    copySharedFile = pkgs.writeScript "copy-shared-file" ''
+      #!${pkgs.runtimeShell}
+      echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
+    '';
+
+    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
+      #!${pkgs.runtimeShell}
+      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
+    '';
+  in ''
+    start_all()
+    nextcloud.wait_for_unit("multi-user.target")
+    nextcloud.succeed("curl -sSf http://nextcloud/login")
+    nextcloud.succeed(
+        "${withRcloneEnv} ${copySharedFile}"
+    )
+    client.wait_for_unit("multi-user.target")
+    client.succeed(
+        "${withRcloneEnv} ${diffSharedFile}"
+    )
+
+    # redis cache should not be empty
+    nextcloud.fail("redis-cli KEYS * | grep -q 'empty array'")
+  '';
+})
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
index 891001e30b231..63e0e2c59639e 100644
--- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -26,6 +26,7 @@ in {
           redis = false;
           memcached = true;
         };
+        database.createLocally = true;
         config = {
           dbtype = "mysql";
           dbname = "nextcloud";
@@ -38,28 +39,6 @@ in {
         };
       };
 
-      services.mysql = {
-        enable = true;
-        settings.mysqld = {
-          bind-address = "127.0.0.1";
-
-          # FIXME(@Ma27) Nextcloud isn't compatible with mariadb 10.6,
-          # this is a workaround.
-          # See https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/22
-          innodb_read_only_compressed = 0;
-        };
-        package = pkgs.mariadb;
-
-        initialScript = pkgs.writeText "mysql-init" ''
-          CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'hunter2';
-          CREATE DATABASE IF NOT EXISTS nextcloud;
-          GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
-            CREATE TEMPORARY TABLES ON nextcloud.* TO 'nextcloud'@'localhost'
-            IDENTIFIED BY 'hunter2';
-          FLUSH privileges;
-        '';
-      };
-
       systemd.services.nextcloud-setup= {
         requires = ["mysql.service"];
         after = ["mysql.service"];
diff --git a/nixos/tests/nghttpx.nix b/nixos/tests/nghttpx.nix
index d83c1c4cae63a..11cac332827dc 100644
--- a/nixos/tests/nghttpx.nix
+++ b/nixos/tests/nghttpx.nix
@@ -54,8 +54,8 @@ in
     testScript = ''
       start_all()
 
-      webserver.wait_for_open_port("80")
-      proxy.wait_for_open_port("80")
+      webserver.wait_for_open_port(80)
+      proxy.wait_for_open_port(80)
       client.wait_until_succeeds("curl -s --fail http://proxy/hello-world.txt")
     '';
   })
diff --git a/nixos/tests/nginx-http3.nix b/nixos/tests/nginx-http3.nix
new file mode 100644
index 0000000000000..319f6aac184ab
--- /dev/null
+++ b/nixos/tests/nginx-http3.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix ({lib, pkgs, ...}:
+let
+  hosts = ''
+    192.168.2.101 acme.test
+  '';
+
+in
+{
+  name = "nginx-http3";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    server = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.101"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+        firewall.allowedTCPPorts = [ 443 ];
+        firewall.allowedUDPPorts = [ 443 ];
+      };
+
+      security.pki.certificates = [
+        (builtins.readFile ./common/acme/server/ca.cert.pem)
+      ];
+
+      services.nginx = {
+        enable = true;
+        package = pkgs.nginxQuic;
+
+        virtualHosts."acme.test" = {
+          onlySSL = true;
+          sslCertificate = ./common/acme/server/acme.test.cert.pem;
+          sslCertificateKey = ./common/acme/server/acme.test.key.pem;
+          http2 = true;
+          http3 = true;
+          reuseport = true;
+          root = lib.mkForce (pkgs.runCommandLocal "testdir2" {} ''
+            mkdir "$out"
+            cat > "$out/index.html" <<EOF
+            <html><body>Hello World!</body></html>
+            EOF
+            cat > "$out/example.txt" <<EOF
+            Check http3 protocol.
+            EOF
+          '');
+        };
+      };
+    };
+
+    client = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curlHTTP3 ];
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.201"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+      };
+
+      security.pki.certificates = [
+        (builtins.readFile ./common/acme/server/ca.cert.pem)
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("nginx")
+    server.wait_for_open_port(443)
+
+    # Check http connections
+    client.succeed("curl --verbose --http3 https://acme.test | grep 'Hello World!'")
+
+    # Check downloadings
+    client.succeed("curl --verbose --http3 https://acme.test/example.txt --output /tmp/example.txt")
+    client.succeed("cat /tmp/example.txt | grep 'Check http3 protocol.'")
+
+    # Check header reading
+    client.succeed("curl --verbose --http3 --head https://acme.test | grep 'content-type'")
+
+    # Check change User-Agent
+    client.succeed("curl --verbose --http3 --user-agent 'Curl test 3.0' https://acme.test")
+    server.succeed("cat /var/log/nginx/access.log | grep 'Curl test 3.0'")
+
+    server.shutdown()
+    client.shutdown()
+  '';
+})
diff --git a/nixos/tests/nginx-modsecurity.nix b/nixos/tests/nginx-modsecurity.nix
new file mode 100644
index 0000000000000..5ceee3787297d
--- /dev/null
+++ b/nixos/tests/nginx-modsecurity.nix
@@ -0,0 +1,39 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "nginx-modsecurity";
+
+  nodes.machine = { config, lib, pkgs, ... }: {
+    services.nginx = {
+      enable = true;
+      additionalModules = [ pkgs.nginxModules.modsecurity-nginx ];
+      virtualHosts.localhost =
+        let modsecurity_conf = pkgs.writeText "modsecurity.conf" ''
+          SecRuleEngine On
+          SecDefaultAction "phase:1,log,auditlog,deny,status:403"
+          SecDefaultAction "phase:2,log,auditlog,deny,status:403"
+          SecRule REQUEST_METHOD   "HEAD"        "id:100, phase:1, block"
+          SecRule REQUEST_FILENAME "secret.html" "id:101, phase:2, block"
+        '';
+        testroot = pkgs.runCommand "testroot" {} ''
+          mkdir -p $out
+          echo "<html><body>Hello World!</body></html>" > $out/index.html
+          echo "s3cret" > $out/secret.html
+        '';
+      in {
+        root = testroot;
+        extraConfig = ''
+          modsecurity on;
+          modsecurity_rules_file ${modsecurity_conf};
+        '';
+      };
+    };
+  };
+  testScript = ''
+    machine.wait_for_unit("nginx")
+
+    response = machine.wait_until_succeeds("curl -fvvv -s http://127.0.0.1/")
+    assert "Hello World!" in response
+
+    machine.fail("curl -fvvv -X HEAD -s http://127.0.0.1/")
+    machine.fail("curl -fvvv -s http://127.0.0.1/secret.html")
+  '';
+})
diff --git a/nixos/tests/nginx-pubhtml.nix b/nixos/tests/nginx-pubhtml.nix
index 6e1e605628e9a..bff24c99d41a4 100644
--- a/nixos/tests/nginx-pubhtml.nix
+++ b/nixos/tests/nginx-pubhtml.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "nginx-pubhtml";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
     services.nginx.enable = true;
     services.nginx.virtualHosts.localhost = {
diff --git a/nixos/tests/nginx-sandbox.nix b/nixos/tests/nginx-sandbox.nix
index 2d512725f2650..92ba30a09cf9f 100644
--- a/nixos/tests/nginx-sandbox.nix
+++ b/nixos/tests/nginx-sandbox.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   # This test checks the creation and reading of a file in sandbox mode. Used simple lua script.
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     nixpkgs.overlays = [
       (self: super: {
         nginx-lua = super.nginx.override {
diff --git a/nixos/tests/nginx-sso.nix b/nixos/tests/nginx-sso.nix
index aeb89859c73f1..221c5f4ed9058 100644
--- a/nixos/tests/nginx-sso.nix
+++ b/nixos/tests/nginx-sso.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = with pkgs.lib.maintainers; [ delroth ];
   };
 
-  machine = {
+  nodes.machine = {
     services.nginx.sso = {
       enable = true;
       configuration = {
diff --git a/nixos/tests/nginx-variants.nix b/nixos/tests/nginx-variants.nix
index 96a9a2c3b8c1d..0faa0127669dd 100644
--- a/nixos/tests/nginx-variants.nix
+++ b/nixos/tests/nginx-variants.nix
@@ -13,7 +13,7 @@ builtins.listToAttrs (
         value = makeTest {
           name = "nginx-variant-${nginxName}";
 
-          machine = { pkgs, ... }: {
+          nodes.machine = { pkgs, ... }: {
             services.nginx = {
               enable = true;
               virtualHosts.localhost.locations."/".return = "200 'foo'";
diff --git a/nixos/tests/nitter.nix b/nixos/tests/nitter.nix
index 0e1a6d150f38e..8bc55ba8c69fc 100644
--- a/nixos/tests/nitter.nix
+++ b/nixos/tests/nitter.nix
@@ -12,7 +12,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
   testScript = ''
     machine.wait_for_unit("nitter.service")
-    machine.wait_for_open_port("80")
+    machine.wait_for_open_port(80)
     machine.succeed("curl --fail http://localhost:80/")
   '';
 })
diff --git a/nixos/tests/nix-ld.nix b/nixos/tests/nix-ld.nix
new file mode 100644
index 0000000000000..ae5297ab87eaa
--- /dev/null
+++ b/nixos/tests/nix-ld.nix
@@ -0,0 +1,20 @@
+import ./make-test-python.nix ({ lib, pkgs, ...} :
+{
+  name = "nix-ld";
+  nodes.machine = { pkgs, ... }: {
+    programs.nix-ld.enable = true;
+    environment.systemPackages = [
+      (pkgs.runCommand "patched-hello" {} ''
+        install -D -m755 ${pkgs.hello}/bin/hello $out/bin/hello
+        patchelf $out/bin/hello --set-interpreter $(cat ${pkgs.nix-ld}/nix-support/ldpath)
+      '')
+    ];
+  };
+  testScript = ''
+    start_all()
+    path = "${pkgs.stdenv.cc}/nix-support/dynamic-linker"
+    with open(path) as f:
+        real_ld = f.read().strip()
+    machine.succeed(f"NIX_LD={real_ld} hello")
+ '';
+})
diff --git a/nixos/tests/nix-serve.nix b/nixos/tests/nix-serve.nix
index ab82f4be43e68..3aa913f81107a 100644
--- a/nixos/tests/nix-serve.nix
+++ b/nixos/tests/nix-serve.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "nix-serve";
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.nix-serve.enable = true;
     environment.systemPackages = [
       pkgs.hello
diff --git a/nixos/tests/nixops/default.nix b/nixos/tests/nixops/default.nix
index b25fc95f4b30b..227b388150737 100644
--- a/nixos/tests/nixops/default.nix
+++ b/nixos/tests/nixops/default.nix
@@ -9,7 +9,7 @@ let
     #  - Alternatively, blocked on a NixOps 2 release
     #    https://github.com/NixOS/nixops/issues/1242
     # stable = testsLegacyNetwork { nixopsPkg = pkgs.nixops; };
-    unstable = testsForPackage { nixopsPkg = pkgs.nixopsUnstable; };
+    unstable = testsForPackage { nixopsPkg = pkgs.nixops_unstable; };
 
     # inherit testsForPackage;
   };
@@ -97,7 +97,7 @@ let
     derivations and all build dependency outputs, all the way down.
   */
   allDrvOutputs = pkg:
-    let name = lib.strings.sanitizeDerivationName "allDrvOutputs-${pkg.pname or pkg.name or "unknown"}";
+    let name = "allDrvOutputs-${pkg.pname or pkg.name or "unknown"}";
     in
     pkgs.runCommand name { refs = pkgs.writeReferencesToFile pkg.drvPath; } ''
       touch $out
diff --git a/nixos/tests/nixos-generate-config.nix b/nixos/tests/nixos-generate-config.nix
index 1dadf4992ed00..e1c2f29e06732 100644
--- a/nixos/tests/nixos-generate-config.nix
+++ b/nixos/tests/nixos-generate-config.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ lib, ... } : {
   name = "nixos-generate-config";
   meta.maintainers = with lib.maintainers; [ basvandijk ];
-  machine = {
+  nodes.machine = {
     system.nixos-generate-config.configuration = ''
       # OVERRIDDEN
       { config, pkgs, ... }: {
diff --git a/nixos/tests/node-red.nix b/nixos/tests/node-red.nix
index 7660bc32f4c93..5f5960d682959 100644
--- a/nixos/tests/node-red.nix
+++ b/nixos/tests/node-red.nix
@@ -19,7 +19,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   testScript = ''
     start_all()
     nodered.wait_for_unit("node-red.service")
-    nodered.wait_for_open_port("1880")
+    nodered.wait_for_open_port(1880)
 
     client.wait_for_unit("multi-user.target")
 
diff --git a/nixos/tests/non-default-filesystems.nix b/nixos/tests/non-default-filesystems.nix
new file mode 100644
index 0000000000000..7fa75aaad724d
--- /dev/null
+++ b/nixos/tests/non-default-filesystems.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+{
+  name = "non-default-filesystems";
+
+  nodes.machine =
+    { config, pkgs, lib, ... }:
+    let
+      disk = config.virtualisation.bootDevice;
+    in
+    {
+      virtualisation.useDefaultFilesystems = false;
+
+      boot.initrd.availableKernelModules = [ "btrfs" ];
+      boot.supportedFilesystems = [ "btrfs" ];
+
+      boot.initrd.postDeviceCommands = ''
+        FSTYPE=$(blkid -o value -s TYPE ${disk} || true)
+        if test -z "$FSTYPE"; then
+          modprobe btrfs
+          ${pkgs.btrfs-progs}/bin/mkfs.btrfs ${disk}
+
+          mkdir /nixos
+          mount -t btrfs ${disk} /nixos
+
+          ${pkgs.btrfs-progs}/bin/btrfs subvolume create /nixos/root
+          ${pkgs.btrfs-progs}/bin/btrfs subvolume create /nixos/home
+
+          umount /nixos
+        fi
+      '';
+
+      virtualisation.fileSystems = {
+        "/" = {
+          device = disk;
+          fsType = "btrfs";
+          options = [ "subvol=/root" ];
+        };
+
+        "/home" = {
+          device = disk;
+          fsType = "btrfs";
+          options = [ "subvol=/home" ];
+        };
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("BTRFS filesystems are mounted correctly"):
+      machine.succeed("grep -E '/dev/vda / btrfs rw,relatime,space_cache=v2,subvolid=[0-9]+,subvol=/root 0 0' /proc/mounts")
+      machine.succeed("grep -E '/dev/vda /home btrfs rw,relatime,space_cache=v2,subvolid=[0-9]+,subvol=/home 0 0' /proc/mounts")
+  '';
+})
diff --git a/nixos/tests/noto-fonts.nix b/nixos/tests/noto-fonts.nix
index 049dc766bd3b7..e4c33fe26a9e6 100644
--- a/nixos/tests/noto-fonts.nix
+++ b/nixos/tests/noto-fonts.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ nickcao midchildan ];
   };
 
-  machine = {
+  nodes.machine = {
     imports = [ ./common/x11.nix ];
     environment.systemPackages = [ pkgs.gnome.gedit ];
     fonts = {
diff --git a/nixos/tests/novacomd.nix b/nixos/tests/novacomd.nix
index b470c117e1e15..d47d212fb2eca 100644
--- a/nixos/tests/novacomd.nix
+++ b/nixos/tests/novacomd.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ dtzWill ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.novacomd.enable = true;
   };
 
diff --git a/nixos/tests/oh-my-zsh.nix b/nixos/tests/oh-my-zsh.nix
index 57a073b086e88..1d5227e36236d 100644
--- a/nixos/tests/oh-my-zsh.nix
+++ b/nixos/tests/oh-my-zsh.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "oh-my-zsh";
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
 
     {
       programs.zsh = {
diff --git a/nixos/tests/ombi.nix b/nixos/tests/ombi.nix
index bfca86af8175d..ce3064ce6ac68 100644
--- a/nixos/tests/ombi.nix
+++ b/nixos/tests/ombi.nix
@@ -12,7 +12,7 @@ with lib;
 
   testScript = ''
     machine.wait_for_unit("ombi.service")
-    machine.wait_for_open_port("5000")
+    machine.wait_for_open_port(5000)
     machine.succeed("curl --fail http://localhost:5000/")
   '';
 })
diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix
index f1a39ad7dde2f..3c388119d5d24 100644
--- a/nixos/tests/openldap.nix
+++ b/nixos/tests/openldap.nix
@@ -25,7 +25,7 @@ in {
     inherit testScript;
     name = "openldap";
 
-    machine = { pkgs, ... }: {
+    nodes.machine = { pkgs, ... }: {
       environment.etc."openldap/root_password".text = "notapassword";
       services.openldap = {
         enable = true;
@@ -65,7 +65,7 @@ in {
     inherit testScript;
     name = "openldap";
 
-    machine = { pkgs, ... }: {
+    nodes.machine = { pkgs, ... }: {
       services.openldap = {
         enable = true;
         logLevel = "stats acl";
@@ -83,7 +83,7 @@ in {
   manualConfigDir = import ./make-test-python.nix ({ pkgs, ... }: {
     name = "openldap";
 
-    machine = { pkgs, ... }: {
+    nodes.machine = { pkgs, ... }: {
       services.openldap = {
         enable = true;
         configDir = "/var/db/slapd.d";
diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix
index 003813379e697..4083f5906d79a 100644
--- a/nixos/tests/openssh.nix
+++ b/nixos/tests/openssh.nix
@@ -80,17 +80,21 @@ in {
 
         client.wait_for_unit("network.target")
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'echo hello world' >&2"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'echo hello world' >&2",
+            timeout=30
         )
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'ulimit -l' | grep 1024"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'ulimit -l' | grep 1024",
+            timeout=30
         )
 
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'echo hello world' >&2"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'echo hello world' >&2",
+            timeout=30
         )
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'ulimit -l' | grep 1024"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'ulimit -l' | grep 1024",
+            timeout=30
         )
 
     with subtest("configured-authkey"):
@@ -99,10 +103,12 @@ in {
         )
         client.succeed("chmod 600 privkey.snakeoil")
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server true"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server true",
+            timeout=30
         )
         client.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server_lazy true"
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server_lazy true",
+            timeout=30
         )
 
     with subtest("localhost-only"):
diff --git a/nixos/tests/opentabletdriver.nix b/nixos/tests/opentabletdriver.nix
index fe345a7bec735..b7583f6dd2648 100644
--- a/nixos/tests/opentabletdriver.nix
+++ b/nixos/tests/opentabletdriver.nix
@@ -6,7 +6,7 @@ in {
     maintainers = with pkgs.lib.maintainers; [ thiagokokada ];
   };
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
     {
       imports = [
         ./common/user-account.nix
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
index 90375450fe1b2..1c89cf8c1c677 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -65,7 +65,7 @@ let
 in {
   name = "os-prober";
 
-  machine = { config, pkgs, ... }: (simpleConfig // {
+  nodes.machine = { config, pkgs, ... }: (simpleConfig // {
       imports = [ ../modules/profiles/installation-device.nix
                   ../modules/profiles/base.nix ];
       virtualisation.memorySize = 1300;
@@ -75,21 +75,30 @@ in {
       # The test cannot access the network, so any packages
       # nixos-rebuild needs must be included in the VM.
       system.extraDependencies = with pkgs;
-        [ sudo
-          libxml2.bin
-          libxslt.bin
+        [
+          brotli
+          brotli.dev
+          brotli.lib
           desktop-file-utils
           docbook5
           docbook_xsl_ns
-          unionfs-fuse
-          ntp
+          grub2
+          kmod.dev
+          libarchive
+          libarchive.dev
+          libxml2.bin
+          libxslt.bin
           nixos-artwork.wallpapers.simple-dark-gray-bottom
-          perlPackages.XMLLibXML
+          ntp
           perlPackages.ListCompare
+          perlPackages.XMLLibXML
+          python3Minimal
           shared-mime-info
+          stdenv
+          sudo
           texinfo
+          unionfs-fuse
           xorg.lndir
-          grub2
 
           # add curl so that rather than seeing the test attempt to download
           # curl's tarball, we see what it's trying to download
diff --git a/nixos/tests/osrm-backend.nix b/nixos/tests/osrm-backend.nix
index 4067d5b1a239a..b0e65a2ae1c10 100644
--- a/nixos/tests/osrm-backend.nix
+++ b/nixos/tests/osrm-backend.nix
@@ -5,7 +5,7 @@ in {
   name = "osrm-backend";
   meta.maintainers = [ lib.maintainers.erictapen ];
 
-  machine = { config, pkgs, ... }:{
+  nodes.machine = { config, pkgs, ... }:{
 
     services.osrm = {
       enable = true;
diff --git a/nixos/tests/overlayfs.nix b/nixos/tests/overlayfs.nix
index 1768f1fea1ed1..6dab6760c5b99 100644
--- a/nixos/tests/overlayfs.nix
+++ b/nixos/tests/overlayfs.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "overlayfs";
   meta.maintainers = with pkgs.lib.maintainers; [ bachp ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.emptyDiskImages = [ 512 ];
     networking.hostId = "deadbeef";
     environment.systemPackages = with pkgs; [ parted ];
diff --git a/nixos/tests/pacemaker.nix b/nixos/tests/pacemaker.nix
new file mode 100644
index 0000000000000..6845576149537
--- /dev/null
+++ b/nixos/tests/pacemaker.nix
@@ -0,0 +1,110 @@
+import ./make-test-python.nix  ({ pkgs, lib, ... }: rec {
+  name = "pacemaker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ astro ];
+  };
+
+  nodes =
+    let
+      node = i: {
+        networking.interfaces.eth1.ipv4.addresses = [ {
+          address = "192.168.0.${toString i}";
+          prefixLength = 24;
+        } ];
+
+        services.corosync = {
+          enable = true;
+          clusterName = "zentralwerk-network";
+          nodelist = lib.imap (i: name: {
+            nodeid = i;
+            inherit name;
+            ring_addrs = [
+              (builtins.head nodes.${name}.networking.interfaces.eth1.ipv4.addresses).address
+            ];
+          }) (builtins.attrNames nodes);
+        };
+        environment.etc."corosync/authkey" = {
+          source = builtins.toFile "authkey"
+            # minimum length: 128 bytes
+            "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest";
+          mode = "0400";
+        };
+
+        services.pacemaker.enable = true;
+
+        # used for pacemaker resource
+        systemd.services.ha-cat = {
+          description = "Highly available netcat";
+          serviceConfig.ExecStart = "${pkgs.netcat}/bin/nc -l discard";
+        };
+      };
+    in {
+      node1 = node 1;
+      node2 = node 2;
+      node3 = node 3;
+    };
+
+  # sets up pacemaker with resources configuration, then crashes a
+  # node and waits for service restart on another node
+  testScript =
+    let
+      resources = builtins.toFile "cib-resources.xml" ''
+        <resources>
+          <primitive id="cat" class="systemd" type="ha-cat">
+            <operations>
+              <op id="stop-cat" name="start" interval="0" timeout="1s"/>
+              <op id="start-cat" name="start" interval="0" timeout="1s"/>
+              <op id="monitor-cat" name="monitor" interval="1s" timeout="1s"/>
+            </operations>
+          </primitive>
+        </resources>
+      '';
+    in ''
+      import re
+      import time
+
+      start_all()
+
+      ${lib.concatMapStrings (node: ''
+        ${node}.wait_until_succeeds("corosync-quorumtool")
+        ${node}.wait_for_unit("pacemaker.service")
+      '') (builtins.attrNames nodes)}
+
+      # No STONITH device
+      node1.succeed("crm_attribute -t crm_config -n stonith-enabled -v false")
+      # Configure the cat resource
+      node1.succeed("cibadmin --replace --scope resources --xml-file ${resources}")
+
+      # wait until the service is started
+      while True:
+        output = node1.succeed("crm_resource -r cat --locate")
+        match = re.search("is running on: (.+)", output)
+        if match:
+          for machine in machines:
+            if machine.name == match.group(1):
+              current_node = machine
+          break
+        time.sleep(1)
+
+      current_node.log("Service running here!")
+      current_node.crash()
+
+      # pick another node that's still up
+      for machine in machines:
+        if machine.booted:
+          check_node = machine
+      # find where the service has been started next
+      while True:
+        output = check_node.succeed("crm_resource -r cat --locate")
+        match = re.search("is running on: (.+)", output)
+        # output will remain the old current_node until the crash is detected by pacemaker
+        if match and match.group(1) != current_node.name:
+          for machine in machines:
+            if machine.name == match.group(1):
+              next_node = machine
+          break
+        time.sleep(1)
+
+      next_node.log("Service migrated here!")
+  '';
+})
diff --git a/nixos/tests/packagekit.nix b/nixos/tests/packagekit.nix
index 020a4e65e6d8f..5769c6c9a8d45 100644
--- a/nixos/tests/packagekit.nix
+++ b/nixos/tests/packagekit.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ peterhoeg ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     environment.systemPackages = with pkgs; [ dbus ];
     services.packagekit = {
       enable = true;
diff --git a/nixos/tests/pam/pam-oath-login.nix b/nixos/tests/pam/pam-oath-login.nix
index 597596b211b11..dd6ef4a0abcb8 100644
--- a/nixos/tests/pam/pam-oath-login.nix
+++ b/nixos/tests/pam/pam-oath-login.nix
@@ -7,7 +7,7 @@ let
   # how many passwords have been made. In this env, we'll always be on
   # the 0th counter, so the password is static.
   #
-  # Generated in nix-shell -p oathToolkit
+  # Generated in nix-shell -p oath-toolkit
   # via: oathtool -v -d6 -w10 cdd4083ef8ff1fa9178c6d46bfb1a3
   # and picking a the first 4:
   oathSnakeOilPassword1 = "143349";
@@ -21,7 +21,7 @@ in
 {
   name = "pam-oath-login";
 
-  machine =
+  nodes.machine =
     { ... }:
     {
       security.pam.oath = {
@@ -77,28 +77,28 @@ in
     machine.screenshot("postboot")
 
     with subtest("Invalid password"):
-        switch_to_tty(2)
-        enter_user_alice(2)
+        switch_to_tty("2")
+        enter_user_alice("2")
 
         machine.send_chars("${oathSnakeOilPassword1}\n")
-        machine.wait_until_tty_matches(2, "Password: ")
+        machine.wait_until_tty_matches("2", "Password: ")
         machine.send_chars("blorg\n")
-        machine.wait_until_tty_matches(2, "Login incorrect")
+        machine.wait_until_tty_matches("2", "Login incorrect")
 
     with subtest("Invalid oath token"):
-        switch_to_tty(3)
-        enter_user_alice(3)
+        switch_to_tty("3")
+        enter_user_alice("3")
 
         machine.send_chars("000000\n")
-        machine.wait_until_tty_matches(3, "Login incorrect")
-        machine.wait_until_tty_matches(3, "login:")
+        machine.wait_until_tty_matches("3", "Login incorrect")
+        machine.wait_until_tty_matches("3", "login:")
 
     with subtest("Happy path: Both passwords are mandatory to get us in"):
-        switch_to_tty(4)
-        enter_user_alice(4)
+        switch_to_tty("4")
+        enter_user_alice("4")
 
         machine.send_chars("${oathSnakeOilPassword2}\n")
-        machine.wait_until_tty_matches(4, "Password: ")
+        machine.wait_until_tty_matches("4", "Password: ")
         machine.send_chars("${alicePassword}\n")
 
         machine.wait_until_succeeds("pgrep -u alice bash")
diff --git a/nixos/tests/pam/pam-u2f.nix b/nixos/tests/pam/pam-u2f.nix
index 0ac6ac17be823..07408dea797e4 100644
--- a/nixos/tests/pam/pam-u2f.nix
+++ b/nixos/tests/pam/pam-u2f.nix
@@ -3,7 +3,7 @@ import ../make-test-python.nix ({ ... }:
 {
   name = "pam-u2f";
 
-  machine =
+  nodes.machine =
     { ... }:
     {
       security.pam.u2f = {
@@ -12,6 +12,7 @@ import ../make-test-python.nix ({ ... }:
         debug = true;
         enable = true;
         interactive = true;
+        origin = "nixos-test";
       };
     };
 
@@ -19,7 +20,7 @@ import ../make-test-python.nix ({ ... }:
     ''
       machine.wait_for_unit("multi-user.target")
       machine.succeed(
-          'egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue" /etc/pam.d/ -R'
+          'egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue.*origin=nixos-test" /etc/pam.d/ -R'
       )
     '';
 })
diff --git a/nixos/tests/pam/pam-ussh.nix b/nixos/tests/pam/pam-ussh.nix
new file mode 100644
index 0000000000000..ba0570dbf97d2
--- /dev/null
+++ b/nixos/tests/pam/pam-ussh.nix
@@ -0,0 +1,70 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  testOnlySSHCredentials = pkgs.runCommand "pam-ussh-test-ca" {
+    nativeBuildInputs = [ pkgs.openssh ];
+  } ''
+    mkdir $out
+    ssh-keygen -t ed25519 -N "" -f $out/ca
+
+    ssh-keygen -t ed25519 -N "" -f $out/alice
+    ssh-keygen -s $out/ca -I "alice user key" -n "alice,root" -V 19700101:forever $out/alice.pub
+
+    ssh-keygen -t ed25519 -N "" -f $out/bob
+    ssh-keygen -s $out/ca -I "bob user key" -n "bob" -V 19700101:forever $out/bob.pub
+  '';
+  makeTestScript = user: pkgs.writeShellScript "pam-ussh-${user}-test-script" ''
+    set -euo pipefail
+
+    eval $(${pkgs.openssh}/bin/ssh-agent)
+
+    mkdir -p $HOME/.ssh
+    chmod 700 $HOME/.ssh
+    cp ${testOnlySSHCredentials}/${user}{,.pub,-cert.pub} $HOME/.ssh
+    chmod 600 $HOME/.ssh/${user}
+    chmod 644 $HOME/.ssh/${user}{,-cert}.pub
+
+    set -x
+
+    ${pkgs.openssh}/bin/ssh-add $HOME/.ssh/${user}
+    ${pkgs.openssh}/bin/ssh-add -l &>2
+
+    exec sudo id -u -n
+  '';
+in {
+  name = "pam-ussh";
+  meta.maintainers = with lib.maintainers; [ lukegb ];
+
+  machine =
+    { ... }:
+    {
+      users.users.alice = { isNormalUser = true; extraGroups = [ "wheel" ]; };
+      users.users.bob = { isNormalUser = true; extraGroups = [ "wheel" ]; };
+
+      security.pam.ussh = {
+        enable = true;
+        authorizedPrincipals = "root";
+        caFile = "${testOnlySSHCredentials}/ca.pub";
+      };
+
+      security.sudo = {
+        enable = true;
+        extraConfig = ''
+          Defaults lecture="never"
+        '';
+      };
+    };
+
+  testScript =
+    ''
+      with subtest("alice should be allowed to escalate to root"):
+        machine.succeed(
+            'su -c "${makeTestScript "alice"}" -l alice | grep root'
+        )
+
+      with subtest("bob should not be allowed to escalate to root"):
+        machine.fail(
+            'su -c "${makeTestScript "bob"}" -l bob | grep root'
+        )
+    '';
+})
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
index 989d29a966dfb..52f85f5c07da8 100644
--- a/nixos/tests/pantheon.nix
+++ b/nixos/tests/pantheon.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     maintainers = teams.pantheon.members;
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [ ./common/user-account.nix ];
diff --git a/nixos/tests/paperless-ng.nix b/nixos/tests/paperless.nix
index 618eeec6b1259..12883cd62c60c 100644
--- a/nixos/tests/paperless-ng.nix
+++ b/nixos/tests/paperless.nix
@@ -1,30 +1,32 @@
 import ./make-test-python.nix ({ lib, ... }: {
-  name = "paperless-ng";
-  meta.maintainers = with lib.maintainers; [ earvstedt Flakebi ];
+  name = "paperless";
+  meta.maintainers = with lib.maintainers; [ erikarvstedt Flakebi ];
 
   nodes.machine = { pkgs, ... }: {
     environment.systemPackages = with pkgs; [ imagemagick jq ];
-    services.paperless-ng = {
+    services.paperless = {
       enable = true;
       passwordFile = builtins.toFile "password" "admin";
     };
   };
 
   testScript = ''
-    machine.wait_for_unit("paperless-ng-consumer.service")
+    import json
 
-    with subtest("Create test doc"):
+    machine.wait_for_unit("paperless-consumer.service")
+
+    with subtest("Add a document via the file system"):
         machine.succeed(
             "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
             "-annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png"
         )
 
     with subtest("Web interface gets ready"):
-        machine.wait_for_unit("paperless-ng-web.service")
+        machine.wait_for_unit("paperless-web.service")
         # Wait until server accepts connections
         machine.wait_until_succeeds("curl -fs localhost:28981")
 
-    with subtest("Create web test doc"):
+    with subtest("Add a document via the web interface"):
         machine.succeed(
             "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
             "-annotate +5+20 'hello web 16-10-2005' /tmp/webdoc.png"
@@ -35,11 +37,8 @@ import ./make-test-python.nix ({ lib, ... }: {
         machine.wait_until_succeeds(
             "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 2))"
         )
-        assert "2005-10-16" in machine.succeed(
-            "curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
-        )
-        assert "2005-10-16" in machine.succeed(
-            "curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[1] | .created'"
-        )
+        docs = json.loads(machine.succeed("curl -u admin:admin -fs localhost:28981/api/documents/"))['results']
+        assert "2005-10-16" in docs[0]['created']
+        assert "2005-10-16" in docs[1]['created']
   '';
 })
diff --git a/nixos/tests/pass-secret-service.nix b/nixos/tests/pass-secret-service.nix
new file mode 100644
index 0000000000000..a85a508bfe16b
--- /dev/null
+++ b/nixos/tests/pass-secret-service.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "pass-secret-service";
+  meta.maintainers = with lib; [ aidalgol ];
+
+  nodes.machine = { nodes, pkgs, ... }:
+    {
+      imports = [ ./common/user-account.nix ];
+
+      services.passSecretService.enable = true;
+
+      environment.systemPackages = [
+        # Create a script that tries to make a request to the D-Bus secrets API.
+        (pkgs.writers.writePython3Bin "secrets-dbus-init"
+          {
+            libraries = [ pkgs.python3Packages.secretstorage ];
+          } ''
+          import secretstorage
+          print("Initializing dbus connection...")
+          connection = secretstorage.dbus_init()
+          print("Requesting default collection...")
+          collection = secretstorage.get_default_collection(connection)
+          print("Done!  dbus-org.freedesktop.secrets should now be active.")
+        '')
+        pkgs.pass
+      ];
+
+      programs.gnupg = {
+        agent.enable = true;
+        agent.pinentryFlavor = "tty";
+        dirmngr.enable = true;
+      };
+    };
+
+  # Some of the commands are run via a virtual console because they need to be
+  # run under a real login session, with D-Bus running in the environment.
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.config.users.users.alice;
+      gpg-uid = "alice@example.net";
+      gpg-pw = "foobar9000";
+      ready-file = "/tmp/secrets-dbus-init.done";
+    in
+    ''
+      # Initialise the pass(1) storage.
+      machine.succeed("""
+        sudo -u alice gpg --pinentry-mode loopback --batch --passphrase ${gpg-pw} \
+        --quick-gen-key ${gpg-uid} \
+      """)
+      machine.succeed("sudo -u alice pass init ${gpg-uid}")
+
+      with subtest("Service is not running on login"):
+          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("${user.password}\n")
+          machine.wait_until_succeeds("pgrep -u alice bash")
+
+          _, output = machine.systemctl("status dbus-org.freedesktop.secrets --no-pager", "alice")
+          assert "Active: inactive (dead)" in output
+
+      with subtest("Service starts after a client tries to talk to the D-Bus API"):
+          machine.send_chars("secrets-dbus-init; touch ${ready-file}\n")
+          machine.wait_for_file("${ready-file}")
+          _, output = machine.systemctl("status dbus-org.freedesktop.secrets --no-pager", "alice")
+          assert "Active: active (running)" in output
+    '';
+})
diff --git a/nixos/tests/pdns-recursor.nix b/nixos/tests/pdns-recursor.nix
index de1b60e0b1c78..14f1b7ea8a35f 100644
--- a/nixos/tests/pdns-recursor.nix
+++ b/nixos/tests/pdns-recursor.nix
@@ -1,12 +1,15 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "powerdns";
+  name = "powerdns-recursor";
 
   nodes.server = { ... }: {
     services.pdns-recursor.enable = true;
+    services.pdns-recursor.exportHosts= true;
+    networking.hosts."192.0.2.1" = [ "example.com" ];
   };
 
   testScript = ''
     server.wait_for_unit("pdns-recursor")
-    server.wait_for_open_port("53")
+    server.wait_for_open_port(53)
+    assert "192.0.2.1" in server.succeed("host example.com localhost")
   '';
 })
diff --git a/nixos/tests/pgadmin4-standalone.nix b/nixos/tests/pgadmin4-standalone.nix
new file mode 100644
index 0000000000000..442570c5306be
--- /dev/null
+++ b/nixos/tests/pgadmin4-standalone.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  # This is seperate from pgadmin4 since we don't want both running at once
+
+  {
+    name = "pgadmin4-standalone";
+    meta.maintainers = with lib.maintainers; [ mkg20001 ];
+
+    nodes.machine = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [
+        curl
+      ];
+
+      services.postgresql = {
+        enable = true;
+
+        authentication = ''
+          host    all             all             localhost               trust
+        '';
+
+        ensureUsers = [
+          {
+            name = "postgres";
+            ensurePermissions = {
+              "DATABASE \"postgres\"" = "ALL PRIVILEGES";
+            };
+          }
+        ];
+      };
+
+      services.pgadmin = {
+        enable = true;
+        initialEmail = "bruh@localhost.de";
+        initialPasswordFile = pkgs.writeText "pw" "bruh2012!";
+      };
+    };
+
+    testScript = ''
+      machine.wait_for_unit("postgresql")
+      machine.wait_for_unit("pgadmin")
+
+      machine.wait_until_succeeds("curl -s localhost:5050")
+    '';
+  })
diff --git a/nixos/tests/pgadmin4.nix b/nixos/tests/pgadmin4.nix
new file mode 100644
index 0000000000000..9f5ac3d8d9229
--- /dev/null
+++ b/nixos/tests/pgadmin4.nix
@@ -0,0 +1,136 @@
+import ./make-test-python.nix ({ pkgs, lib, buildDeps ? [ ], pythonEnv ? [ ], ... }:
+
+  /*
+  This test suite replaces the typical pytestCheckHook function in python
+  packages. Pgadmin4 test suite needs a running and configured postgresql
+  server. This is why this test exists.
+
+  To not repeat all the python dependencies needed, this test is called directly
+  from the pgadmin4 derivation, which also passes the currently
+  used propagatedBuildInputs and any python overrides.
+
+  Unfortunately, there doesn't seem to be an easy way to otherwise include
+  the needed packages here.
+
+  Due the the needed parameters a direct call to "nixosTests.pgadmin4" fails
+  and needs to be called as "pgadmin4.tests"
+
+  */
+
+  let
+    pgadmin4SrcDir = "/pgadmin";
+    pgadmin4Dir = "/var/lib/pgadmin";
+    pgadmin4LogDir = "/var/log/pgadmin";
+
+  in
+  {
+    name = "pgadmin4";
+    meta.maintainers = with lib.maintainers; [ gador ];
+
+    nodes.machine = { pkgs, ... }: {
+      imports = [ ./common/x11.nix ];
+      # needed because pgadmin 6.8 will fail, if those dependencies get updated
+      nixpkgs.overlays = [
+        (self: super: {
+          pythonPackages = pythonEnv;
+        })
+      ];
+
+      environment.systemPackages = with pkgs; [
+        pgadmin4
+        postgresql
+        chromedriver
+        chromium
+        # include the same packages as in pgadmin minus speaklater3
+        (python3.withPackages
+          (ps: buildDeps ++
+            [
+              # test suite package requirements
+              pythonPackages.testscenarios
+              pythonPackages.selenium
+            ])
+        )
+      ];
+      services.postgresql = {
+        enable = true;
+        authentication = ''
+          host    all             all             localhost               trust
+        '';
+        ensureUsers = [
+          {
+            name = "postgres";
+            ensurePermissions = {
+              "DATABASE \"postgres\"" = "ALL PRIVILEGES";
+            };
+          }
+        ];
+      };
+    };
+
+    testScript = ''
+      machine.wait_for_unit("postgresql")
+
+      # pgadmin4 needs its data and log directories
+      machine.succeed(
+          "mkdir -p ${pgadmin4Dir} \
+          && mkdir -p ${pgadmin4LogDir} \
+          && mkdir -p ${pgadmin4SrcDir}"
+      )
+
+      machine.succeed(
+           "tar xvzf ${pkgs.pgadmin4.src} -C ${pgadmin4SrcDir}"
+      )
+
+      machine.wait_for_file("${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/README.md")
+
+      # set paths and config for tests
+      machine.succeed(
+           "cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version} \
+           && cp -v web/regression/test_config.json.in web/regression/test_config.json \
+           && sed -i 's|PostgreSQL 9.4|PostgreSQL|' web/regression/test_config.json \
+           && sed -i 's|/opt/PostgreSQL/9.4/bin/|${pkgs.postgresql}/bin|' web/regression/test_config.json \
+           && sed -i 's|\"headless_chrome\": false|\"headless_chrome\": true|' web/regression/test_config.json"
+      )
+
+      # adapt chrome config to run within a sandbox without GUI
+      # see https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t#50642913
+      # add chrome binary path. use spaces to satisfy python indention (tabs throw an error)
+      # this works for selenium 3 (currently used), but will need to be updated
+      # to work with "from selenium.webdriver.chrome.service import Service" in selenium 4
+      machine.succeed(
+           "cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version} \
+           && sed -i '\|options.add_argument(\"--disable-infobars\")|a \ \ \ \ \ \ \ \ options.binary_location = \"${pkgs.chromium}/bin/chromium\"' web/regression/runtests.py \
+           && sed -i '\|options.add_argument(\"--no-sandbox\")|a \ \ \ \ \ \ \ \ options.add_argument(\"--headless\")' web/regression/runtests.py \
+           && sed -i '\|options.add_argument(\"--disable-infobars\")|a \ \ \ \ \ \ \ \ options.add_argument(\"--disable-dev-shm-usage\")' web/regression/runtests.py \
+           && sed -i 's|(chrome_options=options)|(executable_path=\"${pkgs.chromedriver}/bin/chromedriver\", chrome_options=options)|' web/regression/runtests.py \
+           && sed -i 's|driver_local.maximize_window()||' web/regression/runtests.py"
+      )
+
+      # don't bother to test LDAP authentification
+      # exclude resql test due to recent postgres 14.4 update
+      # see bugreport here https://redmine.postgresql.org/issues/7527
+      with subtest("run browser test"):
+          machine.succeed(
+               'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
+               && python regression/runtests.py \
+               --pkg browser \
+               --exclude browser.tests.test_ldap_login.LDAPLoginTestCase,browser.tests.test_ldap_login,resql'
+          )
+
+      # fontconfig is necessary for chromium to run
+      # https://github.com/NixOS/nixpkgs/issues/136207
+      with subtest("run feature test"):
+          machine.succeed(
+              'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
+               && export FONTCONFIG_FILE=${pkgs.makeFontsConf { fontDirectories = [];}} \
+               && python regression/runtests.py --pkg feature_tests'
+          )
+
+      # reactivate this test again, when the postgres 14.4 test has been fixed
+      # with subtest("run resql test"):
+      #    machine.succeed(
+      #         'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
+      #         && python regression/runtests.py --pkg resql'
+      #    )
+    '';
+  })
diff --git a/nixos/tests/php/fpm.nix b/nixos/tests/php/fpm.nix
index 31a79bb4dbe39..64b61a377e28a 100644
--- a/nixos/tests/php/fpm.nix
+++ b/nixos/tests/php/fpm.nix
@@ -2,7 +2,7 @@ import ../make-test-python.nix ({ pkgs, lib, php, ... }: {
   name = "php-${php.version}-fpm-nginx-test";
   meta.maintainers = lib.teams.php.members;
 
-  machine = { config, lib, pkgs, ... }: {
+  nodes.machine = { config, lib, pkgs, ... }: {
     environment.systemPackages = [ php ];
 
     services.nginx = {
@@ -17,7 +17,7 @@ import ../make-test-python.nix ({ pkgs, lib, php, ... }: {
           locations."~ \\.php$".extraConfig = ''
             fastcgi_pass unix:${config.services.phpfpm.pools.foobar.socket};
             fastcgi_index index.php;
-            include ${pkgs.nginx}/conf/fastcgi_params;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
             include ${pkgs.nginx}/conf/fastcgi.conf;
           '';
           locations."/" = {
diff --git a/nixos/tests/php/httpd.nix b/nixos/tests/php/httpd.nix
index 36d90e72d7d14..b6dfbeeaed527 100644
--- a/nixos/tests/php/httpd.nix
+++ b/nixos/tests/php/httpd.nix
@@ -2,7 +2,7 @@ import ../make-test-python.nix ({ pkgs, lib, php, ... }: {
   name = "php-${php.version}-httpd-test";
   meta.maintainers = lib.teams.php.members;
 
-  machine = { config, lib, pkgs, ... }: {
+  nodes.machine = { config, lib, pkgs, ... }: {
     services.httpd = {
       enable = true;
       adminAddr = "admin@phpfpm";
diff --git a/nixos/tests/php/pcre.nix b/nixos/tests/php/pcre.nix
index 917184b975ec6..57407477f4b8e 100644
--- a/nixos/tests/php/pcre.nix
+++ b/nixos/tests/php/pcre.nix
@@ -5,7 +5,7 @@ import ../make-test-python.nix ({ lib, php, ... }: {
   name = "php-${php.version}-httpd-pcre-jit-test";
   meta.maintainers = lib.teams.php.members;
 
-  machine = { lib, pkgs, ... }: {
+  nodes.machine = { lib, pkgs, ... }: {
     time.timeZone = "UTC";
     services.httpd = {
       enable = true;
diff --git a/nixos/tests/pict-rs.nix b/nixos/tests/pict-rs.nix
index 432fd6a50ccd8..4315e9fb6e90d 100644
--- a/nixos/tests/pict-rs.nix
+++ b/nixos/tests/pict-rs.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
     name = "pict-rs";
     meta.maintainers = with lib.maintainers; [ happysalada ];
 
-    machine = { ... }: {
+    nodes.machine = { ... }: {
       environment.systemPackages = with pkgs; [ curl jq ];
       services.pict-rs.enable = true;
     };
@@ -12,6 +12,6 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       start_all()
 
       machine.wait_for_unit("pict-rs")
-      machine.wait_for_open_port("8080")
+      machine.wait_for_open_port(8080)
     '';
   })
diff --git a/nixos/tests/plasma5-systemd-start.nix b/nixos/tests/plasma5-systemd-start.nix
index 72de19af70cef..f584c1ec137aa 100644
--- a/nixos/tests/plasma5-systemd-start.nix
+++ b/nixos/tests/plasma5-systemd-start.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     maintainers = [ oxalica ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [ ./common/user-account.nix ];
diff --git a/nixos/tests/plasma5.nix b/nixos/tests/plasma5.nix
index 5c7ea602f79e0..b3836cf641d4b 100644
--- a/nixos/tests/plasma5.nix
+++ b/nixos/tests/plasma5.nix
@@ -6,14 +6,17 @@ import ./make-test-python.nix ({ pkgs, ...} :
     maintainers = [ ttuegel ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [ ./common/user-account.nix ];
     services.xserver.enable = true;
     services.xserver.displayManager.sddm.enable = true;
     services.xserver.displayManager.defaultSession = "plasma";
-    services.xserver.desktopManager.plasma5.enable = true;
+    services.xserver.desktopManager.plasma5 = {
+      enable = true;
+      excludePackages = [ pkgs.plasma5Packages.elisa ];
+    };
     services.xserver.displayManager.autoLogin = {
       enable = true;
       user = "alice";
@@ -40,6 +43,9 @@ import ./make-test-python.nix ({ pkgs, ...} :
     with subtest("Check that logging in has given the user ownership of devices"):
         machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
 
+    with subtest("Ensure Elisa is not installed"):
+        machine.fail("which elisa")
+
     with subtest("Run Dolphin"):
         machine.execute("su - ${user.name} -c 'DISPLAY=:0.0 dolphin >&2 &'")
         machine.wait_for_window(" Dolphin")
diff --git a/nixos/tests/plausible.nix b/nixos/tests/plausible.nix
index 58c1dd5cf4a80..ab91e08beb349 100644
--- a/nixos/tests/plausible.nix
+++ b/nixos/tests/plausible.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = [ ma27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.memorySize = 4096;
     services.plausible = {
       enable = true;
diff --git a/nixos/tests/pleroma.nix b/nixos/tests/pleroma.nix
index bf3623fce38b7..8998716243a25 100644
--- a/nixos/tests/pleroma.nix
+++ b/nixos/tests/pleroma.nix
@@ -32,8 +32,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     # 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 "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
@@ -159,7 +158,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
     # 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
+    # Toremove the RELEASE_COOKIE bit when https://github.com/NixOS/nixpkgs/issues/166229 gets fixed.
+    RELEASE_COOKIE="/var/lib/pleroma/.cookie" \
+      pleroma_ctl user new jamy jamy@nixos.test --password 'jamy-password' --moderator --admin -y
   '';
 
   tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
@@ -168,21 +169,6 @@ import ./make-test-python.nix ({ pkgs, ... }:
     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
@@ -194,7 +180,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
       security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
       networking.extraHosts = hosts nodes;
       environment.systemPackages = with pkgs; [
-        custom-toot
+        toot
         send-toot
       ];
     };
diff --git a/nixos/tests/plikd.nix b/nixos/tests/plikd.nix
index 8fec93c01f6bf..97c254a5f7b05 100644
--- a/nixos/tests/plikd.nix
+++ b/nixos/tests/plikd.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ lib, ... }: {
     maintainers = [ freezeboy ];
   };
 
-  machine = { pkgs, ... }: let
+  nodes.machine = { pkgs, ... }: let
   in {
     services.plikd.enable = true;
     environment.systemPackages = [ pkgs.plik ];
@@ -15,7 +15,7 @@ import ./make-test-python.nix ({ lib, ... }: {
     machine.wait_for_unit("plikd")
 
     # Network test
-    machine.wait_for_open_port("8080")
+    machine.wait_for_open_port(8080)
     machine.succeed("curl --fail -v http://localhost:8080")
 
     # Application test
diff --git a/nixos/tests/plotinus.nix b/nixos/tests/plotinus.nix
index af38b41813b7b..b6ebab9b01989 100644
--- a/nixos/tests/plotinus.nix
+++ b/nixos/tests/plotinus.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = pkgs.plotinus.meta.maintainers;
   };
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
 
     { imports = [ ./common/x11.nix ];
diff --git a/nixos/tests/podgrab.nix b/nixos/tests/podgrab.nix
index e927e25fea569..dc9dfebaf49bd 100644
--- a/nixos/tests/podgrab.nix
+++ b/nixos/tests/podgrab.nix
@@ -22,11 +22,11 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     start_all()
 
     default.wait_for_unit("podgrab")
-    default.wait_for_open_port("${toString defaultPort}")
+    default.wait_for_open_port(${toString defaultPort})
     default.succeed("curl --fail http://localhost:${toString defaultPort}")
 
     customized.wait_for_unit("podgrab")
-    customized.wait_for_open_port("${toString customPort}")
+    customized.wait_for_open_port(${toString customPort})
     customized.succeed("curl --fail http://localhost:${toString customPort}")
   '';
 
diff --git a/nixos/tests/podman/default.nix b/nixos/tests/podman/default.nix
index b52a7f060ad66..67c7823c5a316 100644
--- a/nixos/tests/podman/default.nix
+++ b/nixos/tests/podman/default.nix
@@ -126,7 +126,7 @@ import ../make-test-python.nix (
           podman.succeed("docker network create default")
           podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           podman.succeed(
-            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
           )
           podman.succeed("docker ps | grep sleeping")
           podman.succeed("podman ps | grep sleeping")
diff --git a/nixos/tests/podman/tls-ghostunnel.nix b/nixos/tests/podman/tls-ghostunnel.nix
index c0bc47cc40b1b..268a55701ccf2 100644
--- a/nixos/tests/podman/tls-ghostunnel.nix
+++ b/nixos/tests/podman/tls-ghostunnel.nix
@@ -129,7 +129,7 @@ import ../make-test-python.nix (
           podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
 
           client.succeed(
-            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
           )
           client.succeed("docker ps | grep sleeping")
           podman.succeed("docker ps | grep sleeping")
diff --git a/nixos/tests/polaris.nix b/nixos/tests/polaris.nix
new file mode 100644
index 0000000000000..fb2e67f075aac
--- /dev/null
+++ b/nixos/tests/polaris.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "polaris";
+  meta.maintainers = with maintainers; [ pbsds ];
+
+  nodes.machine =
+    { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+      services.polaris = {
+        enable = true;
+        port = 5050;
+        settings.users = [
+          {
+            name = "test_user";
+            password = "very_secret_password";
+            admin = true;
+          }
+        ];
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("polaris.service")
+    machine.wait_for_open_port(5050)
+    machine.succeed("curl http://localhost:5050/api/version")
+    machine.succeed("curl -X GET http://localhost:5050/api/initial_setup -H  'accept: application/json' | jq -e '.has_any_users == true'")
+  '';
+})
diff --git a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
index 5fad1fed75b20..2a6c85a3a9202 100644
--- a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
+++ b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "postfix";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ common/user-account.nix ];
     services.postfix = {
       enable = true;
diff --git a/nixos/tests/postfix.nix b/nixos/tests/postfix.nix
index 6d22b4edba0a2..1dbe6a4c51934 100644
--- a/nixos/tests/postfix.nix
+++ b/nixos/tests/postfix.nix
@@ -5,7 +5,7 @@ in
 import ./make-test-python.nix {
   name = "postfix";
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ common/user-account.nix ];
     services.postfix = {
       enable = true;
diff --git a/nixos/tests/postgresql-wal-receiver.nix b/nixos/tests/postgresql-wal-receiver.nix
index 0e8b3bfd6c34f..ae2708546f5db 100644
--- a/nixos/tests/postgresql-wal-receiver.nix
+++ b/nixos/tests/postgresql-wal-receiver.nix
@@ -31,7 +31,7 @@ let
         name = "postgresql-wal-receiver-${postgresqlPackage}";
         meta.maintainers = with lib.maintainers; [ pacien ];
 
-        machine = { ... }: {
+        nodes.machine = { ... }: {
           services.postgresql = {
             package = pkg;
             enable = true;
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index 2b487c20a6258..7864f5d6ff32c 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -27,7 +27,7 @@ let
       maintainers = [ zagy ];
     };
 
-    machine = {...}:
+    nodes.machine = {...}:
       {
         services.postgresql = {
           enable = true;
diff --git a/nixos/tests/power-profiles-daemon.nix b/nixos/tests/power-profiles-daemon.nix
index e073677bee9d7..278e94711830a 100644
--- a/nixos/tests/power-profiles-daemon.nix
+++ b/nixos/tests/power-profiles-daemon.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   meta = with pkgs.lib.maintainers; {
     maintainers = [ mvnetbiz ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.power-profiles-daemon.enable = true;
     environment.systemPackages = [ pkgs.glib ];
   };
diff --git a/nixos/tests/powerdns.nix b/nixos/tests/powerdns.nix
index d025934ad2b37..70060bad87b69 100644
--- a/nixos/tests/powerdns.nix
+++ b/nixos/tests/powerdns.nix
@@ -10,6 +10,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     services.powerdns.extraConfig = ''
       launch=gmysql
       gmysql-user=pdns
+      zone-cache-refresh-interval=0
     '';
 
     services.mysql = {
diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix
index c0b472638a14d..08773120bc127 100644
--- a/nixos/tests/predictable-interface-names.nix
+++ b/nixos/tests/predictable-interface-names.nix
@@ -16,7 +16,7 @@ in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd }: {
     name = "${if predictable then "" else "un"}predictableInterfaceNames${if withNetworkd then "-with-networkd" else ""}";
     meta = {};
 
-    machine = { lib, ... }: {
+    nodes.machine = { lib, ... }: {
       networking.usePredictableInterfaceNames = lib.mkForce predictable;
       networking.useNetworkd = withNetworkd;
       networking.dhcpcd.enable = !withNetworkd;
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
index c1141465ec24e..fb072514dd90f 100644
--- a/nixos/tests/privacyidea.nix
+++ b/nixos/tests/privacyidea.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
     maintainers = [ fpletz ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     virtualisation.cores = 2;
 
     services.privacyidea = {
diff --git a/nixos/tests/privoxy.nix b/nixos/tests/privoxy.nix
index d16cc498691fd..2d95c4522a01c 100644
--- a/nixos/tests/privoxy.nix
+++ b/nixos/tests/privoxy.nix
@@ -33,7 +33,7 @@ in
     maintainers = [ rnhmjoj ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.nginx.enable = true;
     services.nginx.virtualHosts."example.com" = {
       addSSL = true;
@@ -81,23 +81,23 @@ in
     ''
       with subtest("Privoxy is running"):
           machine.wait_for_unit("privoxy")
-          machine.wait_for_open_port("8118")
+          machine.wait_for_open_port(8118)
           machine.succeed("curl -f http://config.privoxy.org")
 
       with subtest("Privoxy can filter http requests"):
-          machine.wait_for_open_port("80")
+          machine.wait_for_open_port(80)
           assert "great day" in machine.succeed(
               "curl -sfL http://example.com/how-are-you? | tee /dev/stderr"
           )
 
       with subtest("Privoxy can filter https requests"):
-          machine.wait_for_open_port("443")
+          machine.wait_for_open_port(443)
           assert "great day" in machine.succeed(
               "curl -sfL https://example.com/how-are-you? | tee /dev/stderr"
           )
 
       with subtest("Blocks are working"):
-          machine.wait_for_open_port("443")
+          machine.wait_for_open_port(443)
           machine.fail("curl -f https://example.com/ads 1>&2")
           machine.succeed("curl -f https://example.com/PRIVOXY-FORCE/ads 1>&2")
 
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 036c037e426c4..0a1ec824986ab 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -52,7 +52,7 @@ let
     *    testScript = ''
     *      <exporterName>.start()
     *      <exporterName>.wait_for_unit("prometheus-<exporterName>-exporter.service")
-    *      <exporterName>.wait_for_open_port("1234")
+    *      <exporterName>.wait_for_open_port(1234)
     *      <exporterName>.succeed("curl -sSf 'localhost:1234/metrics'")
     *      <exporterName>.shutdown()
     *    '';
@@ -557,10 +557,12 @@ let
         systemd.services.prometheus-mail-exporter = {
           after = [ "postfix.service" ];
           requires = [ "postfix.service" ];
-          preStart = ''
-            mkdir -p -m 0700 mail-exporter/new
-          '';
           serviceConfig = {
+            ExecStartPre = [
+              "${pkgs.writeShellScript "create-maildir" ''
+                mkdir -p -m 0700 mail-exporter/new
+              ''}"
+            ];
             ProtectHome = true;
             ReadOnlyPaths = "/";
             ReadWritePaths = "/var/spool/mail";
@@ -672,7 +674,7 @@ let
             basicAuth.nextcloud-exporter = "snakeoilpw";
             locations."/" = {
               root = "${pkgs.prometheus-nextcloud-exporter.src}/serverinfo/testdata";
-              tryFiles = "/negative-space.xml =404";
+              tryFiles = "/negative-space.json =404";
             };
           };
         };
@@ -933,6 +935,27 @@ let
       '';
     };
 
+    pve = let
+      pveExporterEnvFile = pkgs.writeTextFile {
+        name = "pve.env";
+        text = ''
+          PVE_USER="test_user@pam"
+          PVE_PASSWORD="hunter3"
+          PVE_VERIFY_SSL="false"
+        '';
+      };
+    in {
+      exporterConfig = {
+        enable = true;
+        environmentFile = pveExporterEnvFile;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-pve-exporter.service")
+        wait_for_open_port(9221)
+        wait_until_succeeds("curl localhost:9221")
+      '';
+    };
+
     py-air-control = {
       nodeName = "py_air_control";
       exporterConfig = {
@@ -1156,6 +1179,10 @@ let
     systemd = {
       exporterConfig = {
         enable = true;
+
+        extraFlags = [
+          "--collector.enable-restart-count"
+        ];
       };
       metricProvider = { };
       exporterTest = ''
@@ -1166,6 +1193,11 @@ let
                 'systemd_unit_state{name="basic.target",state="active",type="target"} 1'
             )
         )
+        succeed(
+            "curl -sSf localhost:9558/metrics | grep '{}'".format(
+                'systemd_service_restart_total{state="prometheus-systemd-exporter.service"} 0'
+            )
+        )
       '';
     };
 
@@ -1305,7 +1337,7 @@ mapAttrs
       '';
 
       meta = with maintainers; {
-        maintainers = [ willibutz elseym ];
+        maintainers = [ willibutz ];
       };
     }
   )))
diff --git a/nixos/tests/prowlarr.nix b/nixos/tests/prowlarr.nix
index 4cbca107568f3..144cbd5fc95d5 100644
--- a/nixos/tests/prowlarr.nix
+++ b/nixos/tests/prowlarr.nix
@@ -12,7 +12,7 @@ with lib;
 
   testScript = ''
     machine.wait_for_unit("prowlarr.service")
-    machine.wait_for_open_port("9696")
+    machine.wait_for_open_port(9696)
     machine.succeed("curl --fail http://localhost:9696/")
   '';
 })
diff --git a/nixos/tests/pt2-clone.nix b/nixos/tests/pt2-clone.nix
index 364920c398711..ea4329c4a9806 100644
--- a/nixos/tests/pt2-clone.nix
+++ b/nixos/tests/pt2-clone.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/public-inbox.nix b/nixos/tests/public-inbox.nix
new file mode 100644
index 0000000000000..7de40400fcbf5
--- /dev/null
+++ b/nixos/tests/public-inbox.nix
@@ -0,0 +1,227 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  orga = "example";
+  domain = "${orga}.localdomain";
+
+  tls-cert = pkgs.runCommand "selfSignedCert" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 36500 \
+      -subj '/CN=machine.${domain}'
+    install -D -t $out key.pem cert.pem
+  '';
+in
+{
+  name = "public-inbox";
+
+  meta.maintainers = with pkgs.lib.maintainers; [ julm ];
+
+  machine = { config, pkgs, nodes, ... }: let
+    inherit (config.services) gitolite public-inbox;
+    # Git repositories paths in Gitolite.
+    # Only their baseNameOf is used for configuring public-inbox.
+    repositories = [
+      "user/repo1"
+      "user/repo2"
+    ];
+  in
+  {
+    virtualisation.diskSize = 1 * 1024;
+    virtualisation.memorySize = 1 * 1024;
+    networking.domain = domain;
+
+    security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+    # If using security.acme:
+    #security.acme.certs."${domain}".postRun = ''
+    #  systemctl try-restart public-inbox-nntpd public-inbox-imapd
+    #'';
+
+    services.public-inbox = {
+      enable = true;
+      postfix.enable = true;
+      openFirewall = true;
+      settings.publicinbox = {
+        css = [ "href=https://machine.${domain}/style/light.css" ];
+        nntpserver = [ "nntps://machine.${domain}" ];
+        wwwlisting = "match=domain";
+      };
+      mda = {
+        enable = true;
+        args = [ "--no-precheck" ]; # Allow Bcc:
+      };
+      http = {
+        enable = true;
+        port = "/run/public-inbox-http.sock";
+        #port = 8080;
+        args = ["-W0"];
+        mounts = [
+          "https://machine.${domain}/inbox"
+        ];
+      };
+      nntp = {
+        enable = true;
+        #port = 563;
+        args = ["-W0"];
+        cert = "${tls-cert}/cert.pem";
+        key = "${tls-cert}/key.pem";
+      };
+      imap = {
+        enable = true;
+        #port = 993;
+        args = ["-W0"];
+        cert = "${tls-cert}/cert.pem";
+        key = "${tls-cert}/key.pem";
+      };
+      inboxes = lib.recursiveUpdate (lib.genAttrs (map baseNameOf repositories) (repo: {
+        address = [
+          # Routed to the "public-inbox:" transport in services.postfix.transport
+          "${repo}@${domain}"
+        ];
+        description = ''
+          ${repo}@${domain} :
+          discussions about ${repo}.
+        '';
+        url = "https://machine.${domain}/inbox/${repo}";
+        newsgroup = "inbox.comp.${orga}.${repo}";
+        coderepo = [ repo ];
+      }))
+      {
+        repo2 = {
+          hide = [
+            "imap" # FIXME: doesn't work for IMAP as of public-inbox 1.6.1
+            "manifest"
+            "www"
+          ];
+        };
+      };
+      settings.coderepo = lib.listToAttrs (map (path: lib.nameValuePair (baseNameOf path) {
+        dir = "/var/lib/gitolite/repositories/${path}.git";
+        cgitUrl = "https://git.${domain}/${path}.git";
+      }) repositories);
+    };
+
+    # Use gitolite to store Git repositories listed in coderepo entries
+    services.gitolite = {
+      enable = true;
+      adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJmoTOQnGqX+//us5oye8UuE+tQBx9QEM7PN13jrwgqY root@localhost";
+    };
+    systemd.services.public-inbox-httpd = {
+      serviceConfig.SupplementaryGroups = [ gitolite.group ];
+    };
+
+    # Use nginx as a reverse proxy for public-inbox-httpd
+    services.nginx = {
+      enable = true;
+      recommendedGzipSettings = true;
+      recommendedOptimisation = true;
+      recommendedTlsSettings = true;
+      recommendedProxySettings = true;
+      virtualHosts."machine.${domain}" = {
+        forceSSL = true;
+        sslCertificate = "${tls-cert}/cert.pem";
+        sslCertificateKey = "${tls-cert}/key.pem";
+        locations."/".return = "302 /inbox";
+        locations."= /inbox".return = "302 /inbox/";
+        locations."/inbox".proxyPass = "http://unix:${public-inbox.http.port}:/inbox";
+        # If using TCP instead of a Unix socket:
+        #locations."/inbox".proxyPass = "http://127.0.0.1:${toString public-inbox.http.port}/inbox";
+        # Referred to by settings.publicinbox.css
+        # See http://public-inbox.org/meta/_/text/color/
+        locations."= /style/light.css".alias = pkgs.writeText "light.css" ''
+          * { background:#fff; color:#000 }
+
+          a { color:#00f; text-decoration:none }
+          a:visited { color:#808 }
+
+          *.q { color:#008 }
+
+          *.add { color:#060 }
+          *.del {color:#900 }
+          *.head { color:#000 }
+          *.hunk { color:#960 }
+
+          .hl.num { color:#f30 } /* number */
+          .hl.esc { color:#f0f } /* escape character */
+          .hl.str { color:#f30 } /* string */
+          .hl.ppc { color:#c3c } /* preprocessor */
+          .hl.pps { color:#f30 } /* preprocessor string */
+          .hl.slc { color:#099 } /* single-line comment */
+          .hl.com { color:#099 } /* multi-line comment */
+          /* .hl.opt { color:#ccc } */ /* operator */
+          /* .hl.ipl { color:#ccc } */ /* interpolation */
+
+          /* keyword groups kw[a-z] */
+          .hl.kwa { color:#f90 }
+          .hl.kwb { color:#060 }
+          .hl.kwc { color:#f90 }
+          /* .hl.kwd { color:#ccc } */
+        '';
+      };
+    };
+
+    services.postfix = {
+      enable = true;
+      setSendmail = true;
+      #sslCert = "${tls-cert}/cert.pem";
+      #sslKey = "${tls-cert}/key.pem";
+      recipientDelimiter = "+";
+    };
+
+    environment.systemPackages = [
+      pkgs.mailutils
+      pkgs.openssl
+    ];
+
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("public-inbox-init.service")
+
+    # Very basic check that Gitolite can work;
+    # Gitolite is not needed for the rest of this testScript
+    machine.wait_for_unit("gitolite-init.service")
+
+    # List inboxes through public-inbox-httpd
+    machine.wait_for_unit("nginx.service")
+    machine.succeed("curl -L https://machine.${domain} | grep repo1@${domain}")
+    # The repo2 inbox is hidden
+    machine.fail("curl -L https://machine.${domain} | grep repo2@${domain}")
+    machine.wait_for_unit("public-inbox-httpd.service")
+
+    # Send a mail and read it through public-inbox-httpd
+    # Must work too when using a recipientDelimiter.
+    machine.wait_for_unit("postfix.service")
+    machine.succeed("mail -t <${pkgs.writeText "mail" ''
+      Subject: Testing mail
+      From: root@localhost
+      To: repo1+extension@${domain}
+      Message-ID: <repo1@root-1>
+      Content-Type: text/plain; charset=utf-8
+      Content-Disposition: inline
+
+      This is a testing mail.
+    ''}")
+    machine.sleep(5)
+    machine.succeed("curl -L 'https://machine.${domain}/inbox/repo1/repo1@root-1/T/#u' | grep 'This is a testing mail.'")
+
+    # Read a mail through public-inbox-imapd
+    machine.wait_for_open_port(993)
+    machine.wait_for_unit("public-inbox-imapd.service")
+    machine.succeed("openssl s_client -ign_eof -crlf -connect machine.${domain}:993 <${pkgs.writeText "imap-commands" ''
+      tag login anonymous@${domain} anonymous
+      tag SELECT INBOX.comp.${orga}.repo1.0
+      tag FETCH 1 (BODY[HEADER])
+      tag LOGOUT
+    ''} | grep '^Message-ID: <repo1@root-1>'")
+
+    # TODO: Read a mail through public-inbox-nntpd
+    #machine.wait_for_open_port(563)
+    #machine.wait_for_unit("public-inbox-nntpd.service")
+
+    # Delete a mail.
+    # Note that the use of an extension not listed in the addresses
+    # require to use --all
+    machine.succeed("curl -L https://machine.example.localdomain/inbox/repo1/repo1@root-1/raw | sudo -u public-inbox public-inbox-learn rm --all")
+    machine.fail("curl -L https://machine.example.localdomain/inbox/repo1/repo1@root-1/T/#u | grep 'This is a testing mail.'")
+  '';
+})
diff --git a/nixos/tests/pulseaudio.nix b/nixos/tests/pulseaudio.nix
index 4e2ce679acd71..cfdc61bc6c2b8 100644
--- a/nixos/tests/pulseaudio.nix
+++ b/nixos/tests/pulseaudio.nix
@@ -27,7 +27,7 @@ let
           maintainers = [ synthetica ] ++ pkgs.pulseaudio.meta.maintainers;
         };
 
-        machine = { ... }:
+        nodes.machine = { ... }:
 
           {
             imports = [ ./common/wayland-cage.nix ];
diff --git a/nixos/tests/pykms.nix b/nixos/tests/pykms.nix
new file mode 100644
index 0000000000000..14d776a2f113e
--- /dev/null
+++ b/nixos/tests/pykms.nix
@@ -0,0 +1,14 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  {
+    name = "pykms-test";
+    meta.maintainers = with pkgs.lib.maintainers; [ zopieux ];
+
+    nodes.machine = { config, lib, pkgs, ... }: {
+      services.pykms.enable = true;
+    };
+
+    testScript = ''
+      machine.wait_for_unit("pykms.service")
+      machine.succeed("${pkgs.pykms}/bin/client")
+    '';
+  })
diff --git a/nixos/tests/qboot.nix b/nixos/tests/qboot.nix
index 12aef6decfaed..29d999be58e5b 100644
--- a/nixos/tests/qboot.nix
+++ b/nixos/tests/qboot.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "qboot";
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     virtualisation.bios = pkgs.qboot;
   };
 
diff --git a/nixos/tests/rabbitmq.nix b/nixos/tests/rabbitmq.nix
index 03f1fa46d29e3..f8e8e61c47d25 100644
--- a/nixos/tests/rabbitmq.nix
+++ b/nixos/tests/rabbitmq.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ eelco offline ];
   };
 
-  machine = {
+  nodes.machine = {
     services.rabbitmq = {
       enable = true;
       managementPlugin.enable = true;
@@ -22,6 +22,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     machine.wait_until_succeeds(
         'su -s ${pkgs.runtimeShell} rabbitmq -c "rabbitmqctl status"'
     )
-    machine.wait_for_open_port("15672")
+    machine.wait_for_open_port(15672)
   '';
 })
diff --git a/nixos/tests/radarr.nix b/nixos/tests/radarr.nix
index ed90025ac4205..85fd6572f0617 100644
--- a/nixos/tests/radarr.nix
+++ b/nixos/tests/radarr.nix
@@ -12,7 +12,7 @@ with lib;
 
   testScript = ''
     machine.wait_for_unit("radarr.service")
-    machine.wait_for_open_port("7878")
+    machine.wait_for_open_port(7878)
     machine.succeed("curl --fail http://localhost:7878/")
   '';
 })
diff --git a/nixos/tests/radicale.nix b/nixos/tests/radicale.nix
index 5101628a682c6..66650dce4a008 100644
--- a/nixos/tests/radicale.nix
+++ b/nixos/tests/radicale.nix
@@ -11,7 +11,7 @@ in {
   name = "radicale3";
   meta.maintainers = with lib.maintainers; [ dotlambda ];
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.radicale = {
       enable = true;
       settings = {
diff --git a/nixos/tests/rasdaemon.nix b/nixos/tests/rasdaemon.nix
index e4bd8d96a8d53..7f30a3b81ab55 100644
--- a/nixos/tests/rasdaemon.nix
+++ b/nixos/tests/rasdaemon.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... } : {
     maintainers = [ evils ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
     hardware.rasdaemon = {
       enable = true;
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index 7b70c239ad6ed..abea1657f3ead 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -30,7 +30,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     machine.wait_for_unit("redis-test")
 
     # The unnamed Redis server still opens a port for backward-compatibility
-    machine.wait_for_open_port("6379")
+    machine.wait_for_open_port(6379)
 
     machine.wait_for_file("${redis.servers."".unixSocket}")
     machine.wait_for_file("${redis.servers."test".unixSocket}")
diff --git a/nixos/tests/redmine.nix b/nixos/tests/redmine.nix
index 3866a1f528c05..621b3e6a36eee 100644
--- a/nixos/tests/redmine.nix
+++ b/nixos/tests/redmine.nix
@@ -9,7 +9,7 @@ with pkgs.lib;
 let
   redmineTest = { name, type }: makeTest {
     name = "redmine-${name}";
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       services.redmine = {
         enable = true;
         package = pkgs.redmine;
diff --git a/nixos/tests/restart-by-activation-script.nix b/nixos/tests/restart-by-activation-script.nix
index 0eec292ea9e2b..0ac079e0101e0 100644
--- a/nixos/tests/restart-by-activation-script.nix
+++ b/nixos/tests/restart-by-activation-script.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ das_j ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
 
     systemd.services.restart-me = {
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index 16979eab82170..7523d5e5ed5da 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -1,96 +1,119 @@
 import ./make-test-python.nix (
   { pkgs, ... }:
 
-    let
-      password = "some_password";
-      repository = "/tmp/restic-backup";
-      rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
+  let
+    password = "some_password";
+    repository = "/tmp/restic-backup";
+    repositoryFile = "${pkgs.writeText "repositoryFile" "/tmp/restic-backup-from-file"}";
+    rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
 
-      passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
-      initialize = true;
-      paths = [ "/opt" ];
-      pruneOpts = [
-        "--keep-daily 2"
-        "--keep-weekly 1"
-        "--keep-monthly 1"
-        "--keep-yearly 99"
-      ];
-    in
-      {
-        name = "restic";
+    backupPrepareCommand = ''
+      touch /opt/backupPrepareCommand
+      test ! -e /opt/backupCleanupCommand
+    '';
 
-        meta = with pkgs.lib.maintainers; {
-          maintainers = [ bbigras i077 ];
-        };
+    backupCleanupCommand = ''
+      rm /opt/backupPrepareCommand
+      touch /opt/backupCleanupCommand
+    '';
 
-        nodes = {
-          server =
-            { pkgs, ... }:
-              {
-                services.restic.backups = {
-                  remotebackup = {
-                    inherit repository passwordFile initialize paths pruneOpts;
-                  };
-                  rclonebackup = {
-                    repository = rcloneRepository;
-                    rcloneConfig = {
-                      type = "local";
-                      one_file_system = true;
-                    };
+    passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
+    initialize = true;
+    paths = [ "/opt" ];
+    pruneOpts = [
+      "--keep-daily 2"
+      "--keep-weekly 1"
+      "--keep-monthly 1"
+      "--keep-yearly 99"
+    ];
+  in
+  {
+    name = "restic";
 
-                    # This gets overridden by rcloneConfig.type
-                    rcloneConfigFile = pkgs.writeText "rclone.conf" ''
-                      [local]
-                      type=ftp
-                    '';
-                    inherit passwordFile initialize paths pruneOpts;
-                  };
-                  remoteprune = {
-                    inherit repository passwordFile;
-                    pruneOpts = [ "--keep-last 1" ];
-                  };
-                };
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ bbigras i077 ];
+    };
 
-                environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
+    nodes = {
+      server =
+        { pkgs, ... }:
+        {
+          services.restic.backups = {
+            remotebackup = {
+              inherit repository passwordFile initialize paths pruneOpts backupPrepareCommand backupCleanupCommand;
+            };
+            remotebackup-from-file = {
+              inherit repositoryFile passwordFile initialize paths pruneOpts;
+            };
+            rclonebackup = {
+              repository = rcloneRepository;
+              rcloneConfig = {
+                type = "local";
+                one_file_system = true;
               };
+
+              # This gets overridden by rcloneConfig.type
+              rcloneConfigFile = pkgs.writeText "rclone.conf" ''
+                [local]
+                type=ftp
+              '';
+              inherit passwordFile initialize paths pruneOpts;
+            };
+            remoteprune = {
+              inherit repository passwordFile;
+              pruneOpts = [ "--keep-last 1" ];
+            };
+          };
+
+          environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
         };
+    };
 
-        testScript = ''
-          server.start()
-          server.wait_for_unit("dbus.socket")
-          server.fail(
-              "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots",
-              "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots",
-          )
-          server.succeed(
-              "mkdir -p /opt",
-              "touch /opt/some_file",
-              "mkdir -p /tmp/restic-rclone-backup",
-              "timedatectl set-time '2016-12-13 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
-              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
-              "timedatectl set-time '2017-12-13 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              "timedatectl set-time '2018-12-13 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              "timedatectl set-time '2018-12-14 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              "timedatectl set-time '2018-12-15 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              "timedatectl set-time '2018-12-16 13:45'",
-              "systemctl start restic-backups-remotebackup.service",
-              "systemctl start restic-backups-rclonebackup.service",
-              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
-              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
-              "systemctl start restic-backups-remoteprune.service",
-              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
-          )
-        '';
-      }
+    testScript = ''
+      server.start()
+      server.wait_for_unit("dbus.socket")
+      server.fail(
+          "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots",
+          '${pkgs.restic}/bin/restic --repository-file ${repositoryFile} -p ${passwordFile} snapshots"',
+          "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots",
+      )
+      server.succeed(
+          "mkdir -p /opt",
+          "touch /opt/some_file",
+          "mkdir -p /tmp/restic-rclone-backup",
+          "timedatectl set-time '2016-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-remotebackup-from-file.service",
+          "systemctl start restic-backups-rclonebackup.service",
+          '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+          '${pkgs.restic}/bin/restic --repository-file ${repositoryFile} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+          '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+          "timedatectl set-time '2017-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-rclonebackup.service",
+          "timedatectl set-time '2018-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-rclonebackup.service",
+          "timedatectl set-time '2018-12-14 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-rclonebackup.service",
+          "timedatectl set-time '2018-12-15 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-rclonebackup.service",
+          "timedatectl set-time '2018-12-16 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /opt/backupCleanupCommand",
+          "systemctl start restic-backups-rclonebackup.service",
+          '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+          '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+          "systemctl start restic-backups-remoteprune.service",
+          '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+      )
+    '';
+  }
 )
diff --git a/nixos/tests/retroarch.nix b/nixos/tests/retroarch.nix
index 4c96f9eabc824..c506ed02da89b 100644
--- a/nixos/tests/retroarch.nix
+++ b/nixos/tests/retroarch.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     name = "retroarch";
     meta = with pkgs.lib.maintainers; { maintainers = [ j0hax ]; };
 
-    machine = { ... }:
+    nodes.machine = { ... }:
 
       {
         imports = [ ./common/user-account.nix ];
diff --git a/nixos/tests/riak.nix b/nixos/tests/riak.nix
deleted file mode 100644
index 3dd4e333d6691..0000000000000
--- a/nixos/tests/riak.nix
+++ /dev/null
@@ -1,18 +0,0 @@
-import ./make-test-python.nix ({ lib, pkgs, ... }: {
-  name = "riak";
-  meta = with lib.maintainers; {
-    maintainers = [ Br1ght0ne ];
-  };
-
-  machine = {
-    services.riak.enable = true;
-    services.riak.package = pkgs.riak;
-  };
-
-  testScript = ''
-    machine.start()
-
-    machine.wait_for_unit("riak")
-    machine.wait_until_succeeds("riak ping 2>&1")
-  '';
-})
diff --git a/nixos/tests/rspamd.nix b/nixos/tests/rspamd.nix
index f0ccfe7ea0e6a..26895fbad3f3b 100644
--- a/nixos/tests/rspamd.nix
+++ b/nixos/tests/rspamd.nix
@@ -22,7 +22,7 @@ let
   '';
   simple = name: enableIPv6: makeTest {
     name = "rspamd-${name}";
-    machine = {
+    nodes.machine = {
       services.rspamd.enable = true;
       networking.enableIPv6 = enableIPv6;
     };
@@ -52,7 +52,7 @@ in
   ipv4only = simple "ipv4only" false;
   deprecated = makeTest {
     name = "rspamd-deprecated";
-    machine = {
+    nodes.machine = {
       services.rspamd = {
         enable = true;
         workers.normal.bindSockets = [{
@@ -91,7 +91,7 @@ in
 
   bindports = makeTest {
     name = "rspamd-bindports";
-    machine = {
+    nodes.machine = {
       services.rspamd = {
         enable = true;
         workers.normal.bindSockets = [{
@@ -152,7 +152,7 @@ in
   };
   customLuaRules = makeTest {
     name = "rspamd-custom-lua-rules";
-    machine = {
+    nodes.machine = {
       environment.etc."tests/no-muh.eml".text = ''
         From: Sheep1<bah@example.com>
         To: Sheep2<mah@example.com>
@@ -256,7 +256,7 @@ in
   };
   postfixIntegration = makeTest {
     name = "rspamd-postfix-integration";
-    machine = {
+    nodes.machine = {
       environment.systemPackages = with pkgs; [ msmtp ];
       environment.etc."tests/gtube.eml".text = ''
         From: Sheep1<bah@example.com>
diff --git a/nixos/tests/rstudio-server.nix b/nixos/tests/rstudio-server.nix
index c7ac7670fbd42..dd5fe3e5b4400 100644
--- a/nixos/tests/rstudio-server.nix
+++ b/nixos/tests/rstudio-server.nix
@@ -14,12 +14,6 @@ import ./make-test-python.nix ({ pkgs, ... }:
       };
     };
 
-    users.testuser = {
-      uid = 1000;
-      group = "testgroup";
-    };
-    groups.testgroup.gid = 1000;
-
     testScript = ''
       machine.wait_for_unit("rstudio-server.service")
       machine.succeed("curl -f -vvv -s http://127.0.0.1:8787")
diff --git a/nixos/tests/rsyslogd.nix b/nixos/tests/rsyslogd.nix
index f35db3bd44b83..049acdcd43934 100644
--- a/nixos/tests/rsyslogd.nix
+++ b/nixos/tests/rsyslogd.nix
@@ -11,7 +11,7 @@ with pkgs.lib;
     name = "rsyslogd-test1";
     meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       services.rsyslogd.enable = true;
       services.journald.forwardToSyslog = false;
     };
@@ -27,7 +27,7 @@ with pkgs.lib;
     name = "rsyslogd-test2";
     meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       services.rsyslogd.enable = true;
     };
 
diff --git a/nixos/tests/sabnzbd.nix b/nixos/tests/sabnzbd.nix
index fb35b212b493b..075bd0b1fe093 100644
--- a/nixos/tests/sabnzbd.nix
+++ b/nixos/tests/sabnzbd.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with maintainers; [ jojosch ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.sabnzbd = {
       enable = true;
     };
diff --git a/nixos/tests/schleuder.nix b/nixos/tests/schleuder.nix
new file mode 100644
index 0000000000000..a9e4cc325bc76
--- /dev/null
+++ b/nixos/tests/schleuder.nix
@@ -0,0 +1,128 @@
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+in
+import ./make-test-python.nix {
+  name = "schleuder";
+  nodes.machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmission = true;
+      tlsTrustedAuthorities = "${certs.ca.cert}";
+      sslCert = "${certs.${domain}.cert}";
+      sslKey = "${certs.${domain}.key}";
+      inherit domain;
+      destination = [ domain ];
+      localRecipients = [ "root" "alice" "bob" ];
+    };
+    services.schleuder = {
+      enable = true;
+      # Don't do it like this in production! The point of this setting
+      # is to allow loading secrets from _outside_ the world-readable
+      # Nix store.
+      extraSettingsFile = pkgs.writeText "schleuder-api-keys.yml" ''
+        api:
+          valid_api_keys:
+            - fnord
+      '';
+      lists = [ "security@${domain}" ];
+      settings.api = {
+        tls_cert_file = "${certs.${domain}.cert}";
+        tls_key_file = "${certs.${domain}.key}";
+      };
+    };
+
+    environment.systemPackages = [
+      pkgs.gnupg
+      pkgs.msmtp
+      (pkgs.writeScriptBin "do-test" ''
+        #!${pkgs.runtimeShell}
+        set -exuo pipefail
+
+        # Generate a GPG key with no passphrase and export it
+        sudo -u alice gpg --passphrase-fd 0 --batch --yes --quick-generate-key 'alice@${domain}' rsa4096 sign,encr < <(echo)
+        sudo -u alice gpg --armor --export alice@${domain} > alice.asc
+        # Create a new mailing list with alice as the owner, and alice's key
+        schleuder-cli list new security@${domain} alice@${domain} alice.asc
+
+        # Send an email from a non-member of the list. Use --auto-from so we don't have to specify who it's from twice.
+        msmtp --auto-from security@${domain} --host=${domain} --port=25 --tls --tls-starttls <<EOF
+          Subject: really big security issue!!
+          From: root@${domain}
+
+          I found a big security problem!
+        EOF
+
+        # Wait for delivery
+        (set +o pipefail; journalctl -f -n 1000 -u postfix | grep -m 1 'delivered to maildir')
+
+        # There should be exactly one email
+        mail=(/var/spool/mail/alice/new/*)
+        [[ "''${#mail[@]}" = 1 ]]
+
+        # Find the fingerprint of the mailing list key
+        read list_key_fp address < <(schleuder-cli keys list security@${domain} | grep security@)
+        schleuder-cli keys export security@${domain} $list_key_fp > list.asc
+
+        # Import the key into alice's keyring, so we can verify it as well as decrypting
+        sudo -u alice gpg --import <list.asc
+        # And perform the decryption.
+        sudo -u alice gpg -d $mail >decrypted
+        # And check that the text matches.
+        grep "big security problem" decrypted
+      '')
+
+      # For debugging:
+      # pkgs.vim pkgs.openssl pkgs.sqliteinteractive
+    ];
+
+    security.pki.certificateFiles = [ certs.ca.cert ];
+
+    # Since we don't have internet here, use dnsmasq to provide MX records from /etc/hosts
+    services.dnsmasq = {
+      enable = true;
+      extraConfig = ''
+        selfmx
+      '';
+    };
+
+    networking.extraHosts = ''
+      127.0.0.1 ${domain}
+    '';
+
+    # schleuder-cli's config is not quite optimal in several ways:
+    # - A fingerprint _must_ be pinned, it doesn't even have an option
+    #   to trust the PKI
+    # - It compares certificate fingerprints rather than key
+    #   fingerprints, so renewals break the pin (though that's not
+    #   relevant for this test)
+    # - It compares them as strings, which means we need to match the
+    #   expected format exactly. This means removing the :s and
+    #   lowercasing it.
+    # Refs:
+    # https://0xacab.org/schleuder/schleuder-cli/-/issues/16
+    # https://0xacab.org/schleuder/schleuder-cli/-/blob/f8895b9f47083d8c7b99a2797c93f170f3c6a3c0/lib/schleuder-cli/helper.rb#L230-238
+    systemd.tmpfiles.rules = let cliconfig = pkgs.runCommand "schleuder-cli.yml"
+      {
+        nativeBuildInputs = [ pkgs.jq pkgs.openssl ];
+      } ''
+      fp=$(openssl x509 -in ${certs.${domain}.cert} -noout -fingerprint -sha256 | cut -d = -f 2 | tr -d : | tr 'A-Z' 'a-z')
+      cat > $out <<EOF
+      host: localhost
+      port: 4443
+      tls_fingerprint: "$fp"
+      api_key: fnord
+      EOF
+    ''; in
+      [
+        "L+ /root/.schleuder-cli/schleuder-cli.yml - - - - ${cliconfig}"
+      ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_until_succeeds("nc -z localhost 4443")
+    machine.succeed("do-test")
+  '';
+}
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
index d7c65fa33d67c..c76a9683e66d0 100644
--- a/nixos/tests/sddm.nix
+++ b/nixos/tests/sddm.nix
@@ -12,7 +12,7 @@ let
     default = {
       name = "sddm";
 
-      machine = { ... }: {
+      nodes.machine = { ... }: {
         imports = [ ./common/user-account.nix ];
         services.xserver.enable = true;
         services.xserver.displayManager.sddm.enable = true;
@@ -41,7 +41,7 @@ let
         maintainers = [ ttuegel ];
       };
 
-      machine = { ... }: {
+      nodes.machine = { ... }: {
         imports = [ ./common/user-account.nix ];
         services.xserver.enable = true;
         services.xserver.displayManager = {
diff --git a/nixos/tests/sfxr-qt.nix b/nixos/tests/sfxr-qt.nix
new file mode 100644
index 0000000000000..976b9b11fc66a
--- /dev/null
+++ b/nixos/tests/sfxr-qt.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "sfxr-qt";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.sfxr-qt ];
+  };
+
+  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("sfxr-qt >&2 &")
+
+      machine.wait_for_window(r"sfxr")
+      machine.sleep(10)
+      machine.wait_for_text("requency")
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
index dd2a575b1935a..50a9f71246469 100644
--- a/nixos/tests/shadow.nix
+++ b/nixos/tests/shadow.nix
@@ -39,9 +39,9 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         shadow.wait_until_succeeds("[ $(fgconsole) = 2 ]")
         shadow.wait_for_unit("getty@tty2.service")
         shadow.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
-        shadow.wait_until_tty_matches(2, "login: ")
+        shadow.wait_until_tty_matches("2", "login: ")
         shadow.send_chars("emma\n")
-        shadow.wait_until_tty_matches(2, "login: emma")
+        shadow.wait_until_tty_matches("2", "login: emma")
         shadow.wait_until_succeeds("pgrep login")
         shadow.sleep(2)
         shadow.send_chars("${password1}\n")
@@ -63,9 +63,9 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         shadow.wait_until_succeeds("[ $(fgconsole) = 3 ]")
         shadow.wait_for_unit("getty@tty3.service")
         shadow.wait_until_succeeds("pgrep -f 'agetty.*tty3'")
-        shadow.wait_until_tty_matches(3, "login: ")
+        shadow.wait_until_tty_matches("3", "login: ")
         shadow.send_chars("emma\n")
-        shadow.wait_until_tty_matches(3, "login: emma")
+        shadow.wait_until_tty_matches("3", "login: emma")
         shadow.wait_until_succeeds("pgrep login")
         shadow.sleep(2)
         shadow.send_chars("${password1}\n")
@@ -81,16 +81,16 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         shadow.wait_until_succeeds("[ $(fgconsole) = 4 ]")
         shadow.wait_for_unit("getty@tty4.service")
         shadow.wait_until_succeeds("pgrep -f 'agetty.*tty4'")
-        shadow.wait_until_tty_matches(4, "login: ")
+        shadow.wait_until_tty_matches("4", "login: ")
         shadow.send_chars("emma\n")
-        shadow.wait_until_tty_matches(4, "login: emma")
+        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.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_tty_matches("4", "login: emma")
         shadow.wait_until_succeeds("pgrep login")
         shadow.sleep(2)
         shadow.send_chars("${password3}\n")
@@ -109,11 +109,11 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         shadow.wait_until_succeeds("[ $(fgconsole) = 5 ]")
         shadow.wait_for_unit("getty@tty5.service")
         shadow.wait_until_succeeds("pgrep -f 'agetty.*tty5'")
-        shadow.wait_until_tty_matches(5, "login: ")
+        shadow.wait_until_tty_matches("5", "login: ")
         shadow.send_chars("layla\n")
-        shadow.wait_until_tty_matches(5, "login: layla")
+        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:")
+        shadow.wait_until_tty_matches("5", "login:")
   '';
 })
diff --git a/nixos/tests/shattered-pixel-dungeon.nix b/nixos/tests/shattered-pixel-dungeon.nix
index d4e5de22ab9d0..a256bbdfd7357 100644
--- a/nixos/tests/shattered-pixel-dungeon.nix
+++ b/nixos/tests/shattered-pixel-dungeon.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/shiori.nix b/nixos/tests/shiori.nix
index 418bee43c9394..d0f68b903f8c3 100644
--- a/nixos/tests/shiori.nix
+++ b/nixos/tests/shiori.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
   name = "shiori";
   meta.maintainers = with lib.maintainers; [ minijackson ];
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.shiori.enable = true; };
 
@@ -12,7 +12,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
     authJSON = pkgs.writeText "auth.json" (builtins.toJSON {
       username = "shiori";
       password = "gopher";
-      remember = 1; # hour
       owner = true;
     });
 
diff --git a/nixos/tests/signal-desktop.nix b/nixos/tests/signal-desktop.nix
index 8c72306299230..5e2b648c7cf59 100644
--- a/nixos/tests/signal-desktop.nix
+++ b/nixos/tests/signal-desktop.nix
@@ -16,7 +16,7 @@ in {
     maintainers = [ flokli primeos ];
   };
 
-  machine = { ... }:
+  nodes.machine = { ... }:
 
   {
     imports = [
@@ -60,7 +60,7 @@ in {
     )
     # Only SQLCipher should be able to read the encrypted DB:
     machine.fail(
-        "su - alice -c 'sqlite3 ~/.config/Signal/sql/db.sqlite .databases'"
+        "su - alice -c 'sqlite3 ~/.config/Signal/sql/db.sqlite .tables'"
     )
     print(machine.succeed(
         "su - alice -c 'sqlcipher ~/.config/Signal/sql/db.sqlite'"
diff --git a/nixos/tests/simple.nix b/nixos/tests/simple.nix
index b4d90f750ecfb..c36287b4e843b 100644
--- a/nixos/tests/simple.nix
+++ b/nixos/tests/simple.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ eelco ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
   };
 
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
index 30b8343e2ffee..9b62e4724e757 100644
--- a/nixos/tests/snapcast.nix
+++ b/nixos/tests/snapcast.nix
@@ -19,6 +19,7 @@ in {
         port = port;
         tcp.port = tcpPort;
         http.port = httpPort;
+        openFirewall = true;
         buffer = bufferSize;
         streams = {
           mpd = {
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
index 098d8d9d72f55..651adc8934d38 100644
--- a/nixos/tests/snapper.nix
+++ b/nixos/tests/snapper.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ ... }:
 {
   name = "snapper";
 
-  machine = { pkgs, lib, ... }: {
+  nodes.machine = { pkgs, lib, ... }: {
     boot.initrd.postDeviceCommands = ''
       ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux /dev/vdb
     '';
diff --git a/nixos/tests/soapui.nix b/nixos/tests/soapui.nix
index 76a87ed5efa1c..e4ce3888fd437 100644
--- a/nixos/tests/soapui.nix
+++ b/nixos/tests/soapui.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ asbachb ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/solr.nix b/nixos/tests/solr.nix
index 86efe87c70783..33afe9d788f78 100644
--- a/nixos/tests/solr.nix
+++ b/nixos/tests/solr.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   name = "solr";
   meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
 
-  machine =
+  nodes.machine =
     { config, pkgs, ... }:
     {
       # Ensure the virtual machine has enough memory for Solr to avoid the following error:
diff --git a/nixos/tests/sonarr.nix b/nixos/tests/sonarr.nix
index 764a4d05b381b..bdfc8916cb7f9 100644
--- a/nixos/tests/sonarr.nix
+++ b/nixos/tests/sonarr.nix
@@ -12,7 +12,7 @@ with lib;
 
   testScript = ''
     machine.wait_for_unit("sonarr.service")
-    machine.wait_for_open_port("8989")
+    machine.wait_for_open_port(8989)
     machine.succeed("curl --fail http://localhost:8989/")
   '';
 })
diff --git a/nixos/tests/sourcehut.nix b/nixos/tests/sourcehut.nix
index d1536c5932259..d52fbddd20f39 100644
--- a/nixos/tests/sourcehut.nix
+++ b/nixos/tests/sourcehut.nix
@@ -119,19 +119,24 @@ in
 
   meta.maintainers = [ pkgs.lib.maintainers.tomberek ];
 
-  machine = { config, pkgs, nodes, ... }: {
+  nodes.machine = { config, pkgs, nodes, ... }: {
     # buildsrht needs space
     virtualisation.diskSize = 4 * 1024;
     virtualisation.memorySize = 2 * 1024;
     networking.domain = domain;
     networking.extraHosts = ''
-      ${config.networking.primaryIPAddress} meta.${domain}
       ${config.networking.primaryIPAddress} builds.${domain}
+      ${config.networking.primaryIPAddress} git.${domain}
+      ${config.networking.primaryIPAddress} meta.${domain}
     '';
 
     services.sourcehut = {
       enable = true;
-      services = [ "meta" "builds" ];
+      services = [
+        "builds"
+        "git"
+        "meta"
+      ];
       nginx.enable = true;
       nginx.virtualHost = {
         forceSSL = true;
@@ -148,6 +153,8 @@ in
         #enableWorker = true;
         inherit images;
       };
+      git.enable = true;
+
       settings."sr.ht" = {
         global-domain = config.networking.domain;
         service-key = pkgs.writeText "service-key" "8b327279b77e32a3620e2fc9aabce491cc46e7d821fd6713b2a2e650ce114d01";
@@ -157,7 +164,50 @@ in
         oauth-client-secret = pkgs.writeText "buildsrht-oauth-client-secret" "2260e9c4d9b8dcedcef642860e0504bc";
         oauth-client-id = "299db9f9c2013170";
       };
+      settings."git.sr.ht" = {
+        oauth-client-secret = pkgs.writeText "gitsrht-oauth-client-secret" "3597288dc2c716e567db5384f493b09d";
+        oauth-client-id = "d07cb713d920702e";
+      };
       settings.webhooks.private-key = pkgs.writeText "webhook-key" "Ra3IjxgFiwG9jxgp4WALQIZw/BMYt30xWiOsqD0J7EA=";
+      settings.mail = {
+        smtp-from = "root+hut@${domain}";
+        # WARNING: take care to keep pgp-privkey outside the Nix store in production,
+        # or use LoadCredentialEncrypted=
+        pgp-privkey = toString (pkgs.writeText "sourcehut.pgp-privkey" ''
+          -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+          lFgEYqDRORYJKwYBBAHaRw8BAQdAehGoy36FUx2OesYm07be2rtLyvR5Pb/ltstd
+          Gk7hYQoAAP9X4oPmxxrHN8LewBpWITdBomNqlHoiP7mI0nz/BOPJHxEktDZuaXhv
+          cy90ZXN0cy9zb3VyY2VodXQgPHJvb3QraHV0QHNvdXJjZWh1dC5sb2NhbGRvbWFp
+          bj6IlwQTFgoAPxYhBPqjgjnL8RHN4JnADNicgXaYm0jJBQJioNE5AhsDBQkDwmcA
+          BgsJCAcDCgUVCgkICwUWAwIBAAIeBQIXgAAKCRDYnIF2mJtIySVCAP9e2nHsVHSi
+          2B1YGZpVG7Xf36vxljmMkbroQy+0gBPwRwEAq+jaiQqlbGhQ7R/HMFcAxBIVsq8h
+          Aw1rngsUd0o3dAicXQRioNE5EgorBgEEAZdVAQUBAQdAXZV2Sd5ZNBVTBbTGavMv
+          D6ORrUh8z7TI/3CsxCE7+yADAQgHAAD/c1RU9xH+V/uI1fE7HIn/zL0LUPpsuce2
+          cH++g4u3kBgTOYh+BBgWCgAmFiEE+qOCOcvxEc3gmcAM2JyBdpibSMkFAmKg0TkC
+          GwwFCQPCZwAACgkQ2JyBdpibSMlKagD/cTre6p1m8QuJ7kwmCFRSz5tBzIuYMMgN
+          xtT7dmS91csA/35fWsOykSiFRojQ7ccCSUTHL7ApF2EbL968tP/D2hIG
+          =Hjoc
+          -----END PGP PRIVATE KEY BLOCK-----
+        '');
+        pgp-pubkey = pkgs.writeText "sourcehut.pgp-pubkey" ''
+          -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+          mDMEYqDRORYJKwYBBAHaRw8BAQdAehGoy36FUx2OesYm07be2rtLyvR5Pb/ltstd
+          Gk7hYQq0Nm5peG9zL3Rlc3RzL3NvdXJjZWh1dCA8cm9vdCtodXRAc291cmNlaHV0
+          LmxvY2FsZG9tYWluPoiXBBMWCgA/FiEE+qOCOcvxEc3gmcAM2JyBdpibSMkFAmKg
+          0TkCGwMFCQPCZwAGCwkIBwMKBRUKCQgLBRYDAgEAAh4FAheAAAoJENicgXaYm0jJ
+          JUIA/17acexUdKLYHVgZmlUbtd/fq/GWOYyRuuhDL7SAE/BHAQCr6NqJCqVsaFDt
+          H8cwVwDEEhWyryEDDWueCxR3Sjd0CLg4BGKg0TkSCisGAQQBl1UBBQEBB0BdlXZJ
+          3lk0FVMFtMZq8y8Po5GtSHzPtMj/cKzEITv7IAMBCAeIfgQYFgoAJhYhBPqjgjnL
+          8RHN4JnADNicgXaYm0jJBQJioNE5AhsMBQkDwmcAAAoJENicgXaYm0jJSmoA/3E6
+          3uqdZvELie5MJghUUs+bQcyLmDDIDcbU+3ZkvdXLAP9+X1rDspEohUaI0O3HAklE
+          xy+wKRdhGy/evLT/w9oSBg==
+          =pJD7
+          -----END PGP PUBLIC KEY BLOCK-----
+        '';
+        pgp-key-id = "0xFAA38239CBF111CDE099C00CD89C8176989B48C9";
+      };
     };
 
     networking.firewall.allowedTCPPorts = [ 443 ];
@@ -184,6 +234,7 @@ in
     # Testing metasrht
     machine.wait_for_unit("metasrht-api.service")
     machine.wait_for_unit("metasrht.service")
+    machine.wait_for_unit("metasrht-webhooks.service")
     machine.wait_for_open_port(5000)
     machine.succeed("curl -sL http://localhost:5000 | grep meta.${domain}")
     machine.succeed("curl -sL http://meta.${domain} | grep meta.${domain}")
@@ -193,5 +244,11 @@ in
     machine.wait_for_open_port(5002)
     machine.succeed("curl -sL http://localhost:5002 | grep builds.${domain}")
     #machine.wait_for_unit("buildsrht-worker.service")
+
+    # Testing gitsrht
+    machine.wait_for_unit("gitsrht-api.service")
+    machine.wait_for_unit("gitsrht.service")
+    machine.wait_for_unit("gitsrht-webhooks.service")
+    machine.succeed("curl -sL http://git.${domain} | grep git.${domain}")
   '';
 })
diff --git a/nixos/tests/sssd-ldap.nix b/nixos/tests/sssd-ldap.nix
index 5c58eaef7146f..f816c0652cc55 100644
--- a/nixos/tests/sssd-ldap.nix
+++ b/nixos/tests/sssd-ldap.nix
@@ -13,7 +13,7 @@ in import ./make-test-python.nix ({pkgs, ...}: {
     maintainers = [ bbigras ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.openldap = {
       enable = true;
       settings = {
diff --git a/nixos/tests/sssd.nix b/nixos/tests/sssd.nix
index 5c1abdca6aef4..25527cb59a59b 100644
--- a/nixos/tests/sssd.nix
+++ b/nixos/tests/sssd.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   meta = with pkgs.lib.maintainers; {
     maintainers = [ bbigras ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     services.sssd.enable = true;
   };
 
diff --git a/nixos/tests/starship.nix b/nixos/tests/starship.nix
index 33e9a72f70000..48a4be6caf176 100644
--- a/nixos/tests/starship.nix
+++ b/nixos/tests/starship.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "starship";
   meta.maintainers = pkgs.starship.meta.maintainers;
 
-  machine = {
+  nodes.machine = {
     programs = {
       fish.enable = true;
       zsh.enable = true;
diff --git a/nixos/tests/step-ca.nix b/nixos/tests/step-ca.nix
index b22bcb060f2bf..a855b590232dd 100644
--- a/nixos/tests/step-ca.nix
+++ b/nixos/tests/step-ca.nix
@@ -9,6 +9,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     '';
   in
   {
+    name = "step-ca";
     nodes =
       {
         caserver =
@@ -42,8 +43,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
         caclient =
           { config, pkgs, ... }: {
-            security.acme.server = "https://caserver:8443/acme/acme/directory";
-            security.acme.email = "root@example.org";
+            security.acme.defaults.server = "https://caserver:8443/acme/acme/directory";
+            security.acme.defaults.email = "root@example.org";
             security.acme.acceptTerms = true;
 
             security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
diff --git a/nixos/tests/swap-partition.nix b/nixos/tests/swap-partition.nix
new file mode 100644
index 0000000000000..2279630b57b8f
--- /dev/null
+++ b/nixos/tests/swap-partition.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+{
+  name = "swap-partition";
+
+  nodes.machine =
+    { config, pkgs, lib, ... }:
+    {
+      virtualisation.useDefaultFilesystems = false;
+
+      virtualisation.bootDevice = "/dev/vda1";
+
+      boot.initrd.postDeviceCommands = ''
+        if ! test -b /dev/vda1; then
+          ${pkgs.parted}/bin/parted --script /dev/vda -- mklabel msdos
+          ${pkgs.parted}/bin/parted --script /dev/vda -- mkpart primary 1MiB -250MiB
+          ${pkgs.parted}/bin/parted --script /dev/vda -- mkpart primary -250MiB 100%
+          sync
+        fi
+
+        FSTYPE=$(blkid -o value -s TYPE /dev/vda1 || true)
+        if test -z "$FSTYPE"; then
+          ${pkgs.e2fsprogs}/bin/mke2fs -t ext4 -L root /dev/vda1
+          ${pkgs.util-linux}/bin/mkswap --label swap /dev/vda2
+        fi
+      '';
+
+      virtualisation.fileSystems = {
+        "/" = {
+          device = "/dev/disk/by-label/root";
+          fsType = "ext4";
+        };
+      };
+
+      swapDevices = [
+        {
+          device = "/dev/disk/by-label/swap";
+        }
+      ];
+    };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("Swap is active"):
+      # Doesn't matter if the numbers reported by `free` are slightly off due to unit conversions.
+      machine.succeed("free -h | grep -E 'Swap:\s+2[45][0-9]Mi'")
+  '';
+})
diff --git a/nixos/tests/sway.nix b/nixos/tests/sway.nix
index 1e9e146c4b6ce..52e2c7c99ec4b 100644
--- a/nixos/tests/sway.nix
+++ b/nixos/tests/sway.nix
@@ -4,7 +4,13 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ primeos synthetica ];
   };
 
-  machine = { config, ... }: {
+  # testScriptWithTypes:49: error: Cannot call function of unknown type
+  #           (machine.succeed if succeed else machine.execute)(
+  #           ^
+  # Found 1 error in 1 file (checked 1 source file)
+  skipTypeCheck = true;
+
+  nodes.machine = { config, ... }: {
     # Automatically login on tty1 as a normal user:
     imports = [ ./common/user-account.nix ];
     services.getty.autologinUser = "alice";
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 8e425f0f87796..0198866b6ff83 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -1,6 +1,46 @@
 # Test configuration switching.
 
-import ./make-test-python.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : let
+
+  # Simple service that can either be socket-activated or that will
+  # listen on port 1234 if not socket-activated.
+  # A connection to the socket causes 'hello' to be written to the client.
+  socketTest = pkgs.writeScript "socket-test.py" /* python */ ''
+    #!${pkgs.python3}/bin/python3
+
+    from socketserver import TCPServer, StreamRequestHandler
+    import socket
+    import os
+
+
+    class Handler(StreamRequestHandler):
+        def handle(self):
+            self.wfile.write("hello".encode("utf-8"))
+
+
+    class Server(TCPServer):
+        def __init__(self, server_address, handler_cls):
+            listenFds = os.getenv('LISTEN_FDS')
+            if listenFds is None or int(listenFds) < 1:
+                print(f'Binding to {server_address}')
+                TCPServer.__init__(
+                        self, server_address, handler_cls, bind_and_activate=True)
+            else:
+                TCPServer.__init__(
+                        self, server_address, handler_cls, bind_and_activate=False)
+                # Override socket
+                print(f'Got activated by {os.getenv("LISTEN_FDNAMES")} '
+                      f'with {listenFds} FDs')
+                self.socket = socket.fromfd(3, self.address_family,
+                                            self.socket_type)
+
+
+    if __name__ == "__main__":
+        server = Server(("localhost", 1234), Handler)
+        server.serve_forever()
+  '';
+
+in {
   name = "switch-test";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ gleber das_j ];
@@ -8,8 +48,15 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
   nodes = {
     machine = { pkgs, lib, ... }: {
+      environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff
       users.mutableUsers = false;
 
+      # For boot/switch testing
+      system.build.installBootLoader = lib.mkForce (pkgs.writeShellScript "install-dummy-loader" ''
+        echo "installing dummy bootloader"
+        touch /tmp/bootloader-installed
+      '');
+
       specialisation = rec {
         simpleService.configuration = {
           systemd.services.test = {
@@ -18,10 +65,16 @@ import ./make-test-python.nix ({ pkgs, ...} : {
               Type = "oneshot";
               RemainAfterExit = true;
               ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
             };
           };
         };
 
+        simpleServiceDifferentDescription.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.description = "Test unit";
+        };
+
         simpleServiceModified.configuration = {
           imports = [ simpleService.configuration ];
           systemd.services.test.serviceConfig.X-Test = true;
@@ -70,6 +123,130 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           };
         };
 
+        simpleServiceWithExtraSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraSectionOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test2]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithInstallSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [Install]
+              WantedBy=multi-user.target
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraKey.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        simpleServiceWithExtraKeyOtherValue.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test2";
+        };
+
+        simpleServiceWithExtraKeyOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test2" = "test";
+        };
+
+        simpleServiceReloadTrigger.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/null" ];
+        };
+
+        simpleServiceReloadTriggerModified.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/zero" ];
+        };
+
+        simpleServiceReloadTriggerModifiedAndSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test = {
+            reloadTriggers = [ "/dev/zero" ];
+            serviceConfig."X-Test" = "test";
+          };
+        };
+
+        simpleServiceReloadTriggerModifiedSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        unitWithBackslash.configuration = {
+          systemd.services."escaped\\x2ddash" = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        unitWithBackslashModified.configuration = {
+          imports = [ unitWithBackslash.configuration ];
+          systemd.services."escaped\\x2ddash".serviceConfig.X-Test = "test";
+        };
+
+        unitWithRequirement.configuration = {
+          systemd.services.required-service = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+          systemd.services.test-service = {
+            wantedBy = [ "multi-user.target" ];
+            requires = [ "required-service.service" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        unitWithRequirementModified.configuration = {
+          imports = [ unitWithRequirement.configuration ];
+          systemd.services.required-service.serviceConfig.X-Test = "test";
+          systemd.services.test-service.reloadTriggers = [ "test" ];
+        };
+
+        unitWithRequirementModifiedNostart.configuration = {
+          imports = [ unitWithRequirement.configuration ];
+          systemd.services.test-service.unitConfig.RefuseManualStart = true;
+        };
+
         restart-and-reload-by-activation-script.configuration = {
           systemd.services = rec {
             simple-service = {
@@ -93,6 +270,17 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             no-restart-service = simple-service // {
               restartIfChanged = false;
             };
+
+            reload-triggers = simple-service // {
+              wantedBy = [ "multi-user.target" ];
+            };
+
+            reload-triggers-and-restart-by-as = simple-service;
+
+            reload-triggers-and-restart = simple-service // {
+              stopIfChanged = false; # easier to check for this
+              wantedBy = [ "multi-user.target" ];
+            };
           };
 
           system.activationScripts.restart-and-reload-test = {
@@ -101,19 +289,67 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             text = ''
               if [ "$NIXOS_ACTION" = dry-activate ]; then
                 f=/run/nixos/dry-activation-restart-list
+                g=/run/nixos/dry-activation-reload-list
               else
                 f=/run/nixos/activation-restart-list
+                g=/run/nixos/activation-reload-list
               fi
               cat <<EOF >> "$f"
               simple-service.service
               simple-restart-service.service
               simple-reload-service.service
               no-restart-service.service
+              reload-triggers-and-restart-by-as.service
+              EOF
+
+              cat <<EOF >> "$g"
+              reload-triggers.service
+              reload-triggers-and-restart-by-as.service
+              reload-triggers-and-restart.service
               EOF
             '';
           };
         };
 
+        restart-and-reload-by-activation-script-modified.configuration = {
+          imports = [ restart-and-reload-by-activation-script.configuration ];
+          systemd.services.reload-triggers-and-restart.serviceConfig.X-Modified = "test";
+        };
+
+        simple-socket.configuration = {
+          systemd.services.socket-activated = {
+            description = "A socket-activated service";
+            stopIfChanged = lib.mkDefault false;
+            serviceConfig = {
+              ExecStart = socketTest;
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+          systemd.sockets.socket-activated = {
+            wantedBy = [ "sockets.target" ];
+            listenStreams = [ "/run/test.sock" ];
+            socketConfig.SocketMode = lib.mkDefault "0777";
+          };
+        };
+
+        simple-socket-service-modified.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated.serviceConfig.X-Test = "test";
+        };
+
+        simple-socket-stop-if-changed.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated.stopIfChanged = true;
+        };
+
+        simple-socket-stop-if-changed-and-reloadtrigger.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated = {
+            stopIfChanged = true;
+            reloadTriggers = [ "test" ];
+          };
+        };
+
         mount.configuration = {
           systemd.mounts = [
             {
@@ -158,6 +394,31 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           systemd.timers.test-timer.timerConfig.OnCalendar = lib.mkForce "Fri 2012-11-23 16:00:00";
         };
 
+        hybridSleepModified.configuration = {
+          systemd.targets.hybrid-sleep.unitConfig.X-Test = true;
+        };
+
+        target.configuration = {
+          systemd.targets.test-target.wantedBy = [ "multi-user.target" ];
+          # We use this service to figure out whether the target was modified.
+          # This is the only way because targets are filtered and therefore not
+          # printed when they are started/stopped.
+          systemd.services.test-service = {
+            bindsTo = [ "test-target.target" ];
+            serviceConfig.ExecStart = "${pkgs.coreutils}/bin/sleep infinity";
+          };
+        };
+
+        targetModified.configuration = {
+          imports = [ target.configuration ];
+          systemd.targets.test-target.unitConfig.X-Test = true;
+        };
+
+        targetModifiedStopOnReconfig.configuration = {
+          imports = [ target.configuration ];
+          systemd.targets.test-target.unitConfig.X-StopOnReconfiguration = true;
+        };
+
         path.configuration = {
           systemd.paths.test-watch = {
             wantedBy = [ "paths.target" ];
@@ -166,6 +427,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           systemd.services.test-watch = {
             serviceConfig = {
               Type = "oneshot";
+              RemainAfterExit = true;
               ExecStart = "${pkgs.coreutils}/bin/touch /testpath-modified";
             };
           };
@@ -241,13 +503,38 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             raise Exception(f"Unexpected string '{needle}' was found")
 
 
+    machine.wait_for_unit("multi-user.target")
+
     machine.succeed(
         "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
     )
+    # This tests whether the /etc/os-release parser works which is a fallback
+    # when /etc/NIXOS is missing. If the parser does not work, switch-to-configuration
+    # would fail.
+    machine.succeed("rm /etc/NIXOS")
     machine.succeed(
         "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
     )
 
+
+    with subtest("actions"):
+        # boot action
+        machine.fail("test -f /tmp/bootloader-installed")
+        out = switch_to_specialisation("${machine}", "simpleService", action="boot")
+        assert_contains(out, "installing dummy bootloader")
+        assert_lacks(out, "activating the configuration...")  # good indicator of a system activation
+        machine.succeed("test -f /tmp/bootloader-installed")
+        machine.succeed("rm /tmp/bootloader-installed")
+
+        # switch action
+        machine.fail("test -f /tmp/bootloader-installed")
+        out = switch_to_specialisation("${machine}", "", action="switch")
+        assert_contains(out, "installing dummy bootloader")
+        assert_contains(out, "activating the configuration...")  # good indicator of a system activation
+        machine.succeed("test -f /tmp/bootloader-installed")
+
+        # test and dry-activate actions are tested further down below
+
     with subtest("services"):
         switch_to_specialisation("${machine}", "")
         # Nothing happens when nothing is changed
@@ -258,17 +545,16 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
 
         # Start a simple service
         out = switch_to_specialisation("${machine}", "simpleService")
+        assert_lacks(out, "installing dummy bootloader")  # test does not install a bootloader
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
         assert_contains(out, "reloading the following units: dbus.service\n")  # huh
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_contains(out, "the following new units were started: test.service\n")
-        assert_lacks(out, "as well:")
 
         # Not changing anything doesn't do anything
         out = switch_to_specialisation("${machine}", "simpleService")
@@ -278,7 +564,15 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
+
+        # Only changing the description does nothing
+        out = switch_to_specialisation("${machine}", "simpleServiceDifferentDescription")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
 
         # Restart the simple service
         out = switch_to_specialisation("${machine}", "simpleServiceModified")
@@ -288,7 +582,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_contains(out, "\nstarting the following units: test.service\n")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
 
         # Restart the service with stopIfChanged=false
         out = switch_to_specialisation("${machine}", "simpleServiceNostop")
@@ -298,7 +591,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_contains(out, "\nrestarting the following units: test.service\n")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
 
         # Reload the service with reloadIfChanged=true
         out = switch_to_specialisation("${machine}", "simpleServiceReload")
@@ -308,7 +600,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
 
         # Nothing happens when restartIfChanged=false
         out = switch_to_specialisation("${machine}", "simpleServiceNorestart")
@@ -318,7 +609,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
 
         # Dry mode shows different messages
         out = switch_to_specialisation("${machine}", "simpleService", action="dry-activate")
@@ -328,9 +618,51 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
         assert_contains(out, "would start the following units: test.service\n")
 
+        # Ensure \ works in unit names
+        out = switch_to_specialisation("${machine}", "unitWithBackslash")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: escaped\\x2ddash.service\n")
+
+        out = switch_to_specialisation("${machine}", "unitWithBackslashModified")
+        assert_contains(out, "stopping the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+        # Ensure units that require changed units are properly reloaded
+        out = switch_to_specialisation("${machine}", "unitWithRequirement")
+        assert_contains(out, "stopping the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: required-service.service, test-service.service\n")
+
+        out = switch_to_specialisation("${machine}", "unitWithRequirementModified")
+        assert_contains(out, "stopping the following units: required-service.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: required-service.service, test-service.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+        # Unless the unit asks to be not restarted
+        out = switch_to_specialisation("${machine}", "unitWithRequirementModifiedNostart")
+        assert_contains(out, "stopping the following units: required-service.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: required-service.service\n")
+        assert_lacks(out, "the following new units were started:")
+
     with subtest("failing units"):
         # Let the simple service fail
         switch_to_specialisation("${machine}", "simpleServiceModified")
@@ -343,7 +675,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "the following new units were started:")
         assert_contains(out, "warning: the following units failed: test.service\n")
         assert_contains(out, "Main PID:")  # output of systemctl
-        assert_lacks(out, "as well:")
 
         # A unit that gets into autorestart without failing is not treated as failed
         out = switch_to_specialisation("${machine}", "autorestartService")
@@ -353,7 +684,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_contains(out, "the following new units were started: autorestart.service\n")
-        assert_lacks(out, "as well:")
         machine.systemctl('stop autorestart.service')  # cancel the 20y timer
 
         # Switching to the same system should do nothing (especially not treat the unit as failed)
@@ -364,7 +694,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_contains(out, "the following new units were started: autorestart.service\n")
-        assert_lacks(out, "as well:")
         machine.systemctl('stop autorestart.service')  # cancel the 20y timer
 
         # If systemd thinks the unit has failed and is in autorestart, we should show it as failed
@@ -377,7 +706,118 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "the following new units were started:")
         assert_contains(out, "warning: the following units failed: autorestart.service\n")
         assert_contains(out, "Main PID:")  # output of systemctl
-        assert_lacks(out, "as well:")
+
+    with subtest("unit file parser"):
+        # Switch to a well-known state
+        switch_to_specialisation("${machine}", "simpleServiceNostop")
+
+        # Add a section
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSectionOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # [Install] section is ignored
+        out = switch_to_specialisation("${machine}", "simpleServiceWithInstallSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Add a key
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKey")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Change its value
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherValue")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Add a reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTrigger")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Modify the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Modify the reload trigger and something else
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedAndSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
 
     with subtest("restart and reload by activation script"):
         switch_to_specialisation("${machine}", "simpleServiceNorestart")
@@ -386,25 +826,98 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "NOT restarting the following changed units:")
         assert_lacks(out, "reloading the following units:")
         assert_lacks(out, "restarting the following units:")
-        assert_contains(out, "\nstarting the following units: no-restart-service.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
-        assert_lacks(out, "as well:")
+        assert_contains(out, "\nstarting the following units: no-restart-service.service, reload-triggers-and-restart-by-as.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "the following new units were started: no-restart-service.service, reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
         # Switch to the same system where the example services get restarted
-        # by the activation script
+        # and reloaded by the activation script
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
-        assert_contains(out, "reloading the following units: simple-reload-service.service\n")
-        assert_contains(out, "restarting the following units: simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "reloading the following units: reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, simple-restart-service.service, simple-service.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # Switch to the same system and see if the service gets restarted when it's modified
+        # while the fact that it's supposed to be reloaded by the activation script is ignored.
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script-modified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
         assert_lacks(out, "\nstarting the following units:")
-        assert_lacks(out, "as well:")
+        assert_lacks(out, "the following new units were started:")
         # The same, but in dry mode
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script", action="dry-activate")
         assert_lacks(out, "would stop the following units:")
         assert_lacks(out, "would NOT stop the following changed units:")
-        assert_contains(out, "would reload the following units: simple-reload-service.service\n")
-        assert_contains(out, "would restart the following units: simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "would reload the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "would restart the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
         assert_lacks(out, "\nwould start the following units:")
-        assert_lacks(out, "as well:")
+
+    with subtest("socket-activated services"):
+        # Socket-activated services don't get started, just the socket
+        machine.fail("[ -S /run/test.sock ]")
+        out = switch_to_specialisation("${machine}", "simple-socket")
+        # assert_lacks(out, "stopping the following units:") not relevant
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: socket-activated.socket\n")
+        machine.succeed("[ -S /run/test.sock ]")
+
+        # Changing a non-activated service does nothing
+        out = switch_to_specialisation("${machine}", "simple-socket-service-modified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # The unit is properly activated when the socket is accessed
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated")  # idk how that would happen tbh
+
+        # Changing an activated service with stopIfChanged=false restarts the service
+        out = switch_to_specialisation("${machine}", "simple-socket")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: socket-activated.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
+
+        # Changing an activated service with stopIfChanged=true stops the service and
+        # socket and starts the socket
+        out = switch_to_specialisation("${machine}", "simple-socket-stop-if-changed")
+        assert_contains(out, "stopping the following units: socket-activated.service, socket-activated.socket\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: socket-activated.socket\n")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
+
+        # Changing a reload trigger of a socket-activated unit only reloads it
+        out = switch_to_specialisation("${machine}", "simple-socket-stop-if-changed-and-reloadtrigger")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: socket-activated.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units: socket-activated.socket")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
 
     with subtest("mounts"):
         switch_to_specialisation("${machine}", "mount")
@@ -417,7 +930,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
         # It changed
         out = machine.succeed("mount | grep 'on /testmount'")
         assert_contains(out, "size=10240k")
@@ -428,15 +940,64 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_contains(out, "OnCalendar=2014-03-25 02:59:56 UTC")
         out = switch_to_specialisation("${machine}", "timerModified")
         assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following units:")
         assert_lacks(out, "reloading the following units:")
-        assert_contains(out, "restarting the following units: test-timer.timer\n")
+        assert_contains(out, "\nrestarting the following units: test-timer.timer\n")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
-        assert_lacks(out, "as well:")
         # It changed
         out = machine.succeed("systemctl show test-timer.timer")
         assert_contains(out, "OnCalendar=Fri 2012-11-23 16:00:00")
 
+    with subtest("targets"):
+        # Modifying some special targets like hybrid-sleep.target does nothing
+        out = switch_to_specialisation("${machine}", "hybridSleepModified")
+        assert_contains(out, "stopping the following units: test-timer.timer\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Adding a new target starts it
+        out = switch_to_specialisation("${machine}", "target")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-target.target\n")
+
+        # Changing a target doesn't print anything because the unit is filtered
+        machine.systemctl("start test-service.service")
+        out = switch_to_specialisation("${machine}", "targetModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("systemctl is-active test-service.service")  # target was not restarted
+
+        # With X-StopOnReconfiguration, the target gets stopped and started
+        out = switch_to_specialisation("${machine}", "targetModifiedStopOnReconfig")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.fail("systemctl is-active test-service.servce")  # target was restarted
+
+        # Remove the target by switching to the old specialisation
+        out = switch_to_specialisation("${machine}", "timerModified")
+        assert_contains(out, "stopping the following units: test-target.target\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-timer.timer\n")
+
     with subtest("paths"):
         out = switch_to_specialisation("${machine}", "path")
         assert_contains(out, "stopping the following units: test-timer.timer\n")
@@ -444,14 +1005,14 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         assert_lacks(out, "reloading the following units:")
         assert_lacks(out, "\nrestarting the following units:")
         assert_lacks(out, "\nstarting the following units:")
-        assert_contains(out, "the following new units were started: test-watch.path")
-        assert_lacks(out, "as well:")
+        assert_contains(out, "the following new units were started: test-watch.path\n")
         machine.fail("test -f /testpath-modified")
 
         # touch the file, unit should be triggered
         machine.succeed("touch /testpath")
         machine.wait_until_succeeds("test -f /testpath-modified")
         machine.succeed("rm /testpath /testpath-modified")
+        machine.systemctl("stop test-watch.service")
         switch_to_specialisation("${machine}", "pathModified")
         machine.succeed("touch /testpath")
         machine.fail("test -f /testpath-modified")
@@ -466,8 +1027,21 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     with subtest("slices"):
         machine.succeed("echo 0 > /proc/sys/vm/panic_on_oom")  # allow OOMing
         out = switch_to_specialisation("${machine}", "slice")
+        # assert_lacks(out, "stopping the following units:") not relevant
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
         machine.fail("systemctl start testservice.service")
+
         out = switch_to_specialisation("${machine}", "sliceModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
         machine.succeed("systemctl start testservice.service")
         machine.succeed("echo 1 > /proc/sys/vm/panic_on_oom")  # disallow OOMing
   '';
diff --git a/nixos/tests/sympa.nix b/nixos/tests/sympa.nix
index aad7c95b6c99c..76ca17d0a1890 100644
--- a/nixos/tests/sympa.nix
+++ b/nixos/tests/sympa.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "sympa";
   meta.maintainers = with lib.maintainers; [ mmilata ];
 
-  machine =
+  nodes.machine =
     { ... }:
     {
 
diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix
index 8b60ad7faf090..fcd90739e6a55 100644
--- a/nixos/tests/syncthing-init.nix
+++ b/nixos/tests/syncthing-init.nix
@@ -6,7 +6,7 @@ in {
   name = "syncthing-init";
   meta.maintainers = with pkgs.lib.maintainers; [ lassulus ];
 
-  machine = {
+  nodes.machine = {
     services.syncthing = {
       enable = true;
       devices.testDevice = {
diff --git a/nixos/tests/syncthing-relay.nix b/nixos/tests/syncthing-relay.nix
index a0233c969ec06..3d70b1eda7b2a 100644
--- a/nixos/tests/syncthing-relay.nix
+++ b/nixos/tests/syncthing-relay.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
   name = "syncthing-relay";
   meta.maintainers = with pkgs.lib.maintainers; [ delroth ];
 
-  machine = {
+  nodes.machine = {
     environment.systemPackages = [ pkgs.jq ];
     services.syncthing.relay = {
       enable = true;
diff --git a/nixos/tests/systemd-analyze.nix b/nixos/tests/systemd-analyze.nix
index 186f5aee7b85e..31588e2b41aa5 100644
--- a/nixos/tests/systemd-analyze.nix
+++ b/nixos/tests/systemd-analyze.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
     maintainers = [ raskin ];
   };
 
-  machine =
+  nodes.machine =
     { pkgs, lib, ... }:
     { boot.kernelPackages = lib.mkIf latestKernel pkgs.linuxPackages_latest;
       sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
diff --git a/nixos/tests/systemd-binfmt.nix b/nixos/tests/systemd-binfmt.nix
index a3a6efac3e4dc..b16fda0ddb1a6 100644
--- a/nixos/tests/systemd-binfmt.nix
+++ b/nixos/tests/systemd-binfmt.nix
@@ -31,7 +31,7 @@ let
 in {
   basic = makeTest {
     name = "systemd-binfmt";
-    machine = {
+    nodes.machine = {
       boot.binfmt.emulatedSystems = [
         "armv7l-linux"
         "aarch64-linux"
@@ -56,7 +56,7 @@ in {
 
   preserveArgvZero = makeTest {
     name = "systemd-binfmt-preserve-argv0";
-    machine = {
+    nodes.machine = {
       boot.binfmt.emulatedSystems = [
         "aarch64-linux"
       ];
@@ -71,7 +71,7 @@ in {
 
   ldPreload = makeTest {
     name = "systemd-binfmt-ld-preload";
-    machine = {
+    nodes.machine = {
       boot.binfmt.emulatedSystems = [
         "aarch64-linux"
       ];
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
index 51cfd82e6c4bb..039e6bdd9d5ab 100644
--- a/nixos/tests/systemd-boot.nix
+++ b/nixos/tests/systemd-boot.nix
@@ -20,7 +20,7 @@ in
     name = "systemd-boot";
     meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
-    machine = common;
+    nodes.machine = common;
 
     testScript = ''
       machine.start()
@@ -44,7 +44,7 @@ in
     name = "systemd-boot-specialisation";
     meta.maintainers = with pkgs.lib.maintainers; [ lukegb ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       specialisation.something.configuration = {};
     };
@@ -67,7 +67,7 @@ in
     name = "systemd-boot-fallback";
     meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.efi.canTouchEfiVariables = mkForce false;
     };
@@ -93,7 +93,7 @@ in
     name = "systemd-boot-update";
     meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
 
-    machine = common;
+    nodes.machine = common;
 
     testScript = ''
       machine.succeed("mount -o remount,rw /boot")
@@ -115,7 +115,7 @@ in
     name = "systemd-boot-memtest86";
     meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.systemd-boot.memtest86.enable = true;
       nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
@@ -133,7 +133,7 @@ in
     name = "systemd-boot-netbootxyz";
     meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.systemd-boot.netbootxyz.enable = true;
     };
@@ -148,7 +148,7 @@ in
     name = "systemd-boot-entry-filename";
     meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.systemd-boot.memtest86.enable = true;
       boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf";
@@ -168,7 +168,7 @@ in
     name = "systemd-boot-extra-entries";
     meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.systemd-boot.extraEntries = {
         "banana.conf" = ''
@@ -187,7 +187,7 @@ in
     name = "systemd-boot-extra-files";
     meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
 
-    machine = { pkgs, lib, ... }: {
+    nodes.machine = { pkgs, lib, ... }: {
       imports = [ common ];
       boot.loader.systemd-boot.extraFiles = {
         "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix
index 8fafb11e1e8cd..bde5b770ea50d 100644
--- a/nixos/tests/systemd-confinement.nix
+++ b/nixos/tests/systemd-confinement.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix {
   name = "systemd-confinement";
 
-  machine = { pkgs, lib, ... }: let
+  nodes.machine = { pkgs, lib, ... }: let
     testServer = pkgs.writeScript "testserver.sh" ''
       #!${pkgs.runtimeShell}
       export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"}
@@ -17,15 +17,19 @@ import ./make-test-python.nix {
       exit "''${ret:-1}"
     '';
 
-    mkTestStep = num: { config ? {}, testScript }: {
-      systemd.sockets."test${toString num}" = {
+    mkTestStep = num: {
+      testScript,
+      config ? {},
+      serviceName ? "test${toString num}",
+    }: {
+      systemd.sockets.${serviceName} = {
         description = "Socket for Test Service ${toString num}";
         wantedBy = [ "sockets.target" ];
         socketConfig.ListenStream = "/run/test${toString num}.sock";
         socketConfig.Accept = true;
       };
 
-      systemd.services."test${toString num}@" = {
+      systemd.services."${serviceName}@" = {
         description = "Confined Test Service ${toString num}";
         confinement = (config.confinement or {}) // { enable = true; };
         serviceConfig = (config.serviceConfig or {}) // {
@@ -135,6 +139,16 @@ import ./make-test-python.nix {
               machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek')
         '';
       }
+      { serviceName = "shipped-unitfile";
+        config.confinement.mode = "chroot-only";
+        testScript = ''
+          with subtest("check if shipped unit file still works"):
+              machine.succeed(
+                  'chroot-exec \'kill -9 $$ 2>&1 || :\' | '
+                  'grep -q "Too many levels of symbolic links"'
+              )
+        '';
+      }
     ];
 
     options.__testSteps = lib.mkOption {
@@ -143,6 +157,15 @@ import ./make-test-python.nix {
     };
 
     config.environment.systemPackages = lib.singleton testClient;
+    config.systemd.packages = lib.singleton (pkgs.writeTextFile {
+      name = "shipped-unitfile";
+      destination = "/etc/systemd/system/shipped-unitfile@.service";
+      text = ''
+        [Service]
+        SystemCallFilter=~kill
+        SystemCallErrorNumber=ELOOP
+      '';
+    });
 
     config.users.groups.chroot-testgroup = {};
     config.users.users.chroot-testuser = {
diff --git a/nixos/tests/systemd-cryptenroll.nix b/nixos/tests/systemd-cryptenroll.nix
index 49634ef65672c..055ae7d1681f2 100644
--- a/nixos/tests/systemd-cryptenroll.nix
+++ b/nixos/tests/systemd-cryptenroll.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ ymatsiuk ];
   };
 
-  machine = { pkgs, lib, ... }: {
+  nodes.machine = { pkgs, lib, ... }: {
     environment.systemPackages = [ pkgs.cryptsetup ];
     virtualisation = {
       emptyDiskImages = [ 512 ];
diff --git a/nixos/tests/systemd-escaping.nix b/nixos/tests/systemd-escaping.nix
new file mode 100644
index 0000000000000..29d2ed1aa3523
--- /dev/null
+++ b/nixos/tests/systemd-escaping.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  echoAll = pkgs.writeScript "echo-all" ''
+    #! ${pkgs.runtimeShell}
+    for s in "$@"; do
+      printf '%s\n' "$s"
+    done
+  '';
+  # deliberately using a local empty file instead of pkgs.emptyFile to have
+  # a non-store path in the test
+  args = [ "a%Nything" "lang=\${LANG}" ";" "/bin/sh -c date" ./empty-file 4.2 23 ];
+in
+{
+  name = "systemd-escaping";
+
+  nodes.machine = { pkgs, lib, utils, ... }: {
+    systemd.services.echo =
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ [] ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ {} ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ null ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ false ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ (_:_) ])).success;
+      { description = "Echo to the journal";
+        serviceConfig.Type = "oneshot";
+        serviceConfig.ExecStart = ''
+          ${echoAll} ${utils.escapeSystemdExecArgs args}
+        '';
+      };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("systemctl start echo.service")
+    # skip the first 'Starting <service> ...' line
+    logs = machine.succeed("journalctl -u echo.service -o cat").splitlines()[1:]
+    assert "a%Nything" == logs[0]
+    assert "lang=''${LANG}" == logs[1]
+    assert ";" == logs[2]
+    assert "/bin/sh -c date" == logs[3]
+    assert "/nix/store/ij3gw72f4n5z4dz6nnzl1731p9kmjbwr-empty-file" == logs[4]
+    assert "4.2" in logs[5] # toString produces extra fractional digits!
+    assert "23" == logs[6]
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-btrfs-raid.nix b/nixos/tests/systemd-initrd-btrfs-raid.nix
new file mode 100644
index 0000000000000..40fd2d4dc611c
--- /dev/null
+++ b/nixos/tests/systemd-initrd-btrfs-raid.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-btrfs-raid";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+
+    environment.systemPackages = with pkgs; [ btrfs-progs ];
+    boot.initrd.systemd = {
+      enable = true;
+      emergencyAccess = true;
+    };
+
+    specialisation.boot-btrfs-raid.configuration = {
+      fileSystems = lib.mkVMOverride {
+        "/".fsType = lib.mkForce "btrfs";
+      };
+      virtualisation.bootDevice = "/dev/vdc";
+    };
+  };
+
+  testScript = ''
+    # Create RAID
+    machine.succeed("mkfs.btrfs -d raid0 /dev/vdc /dev/vdd")
+    machine.succeed("mkdir -p /mnt && mount /dev/vdc /mnt && echo hello > /mnt/test && umount /mnt")
+
+    # Boot from the RAID
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-btrfs-raid.conf")
+    machine.succeed("sync")
+    machine.crash()
+    machine.wait_for_unit("multi-user.target")
+
+    # Ensure we have successfully booted from the RAID
+    assert "(initrd)" in machine.succeed("systemd-analyze")  # booted with systemd in stage 1
+    assert "/dev/vdc on / type btrfs" in machine.succeed("mount")
+    assert "hello" in machine.succeed("cat /test")
+    assert "Total devices 2" in machine.succeed("btrfs filesystem show")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-luks-keyfile.nix b/nixos/tests/systemd-initrd-luks-keyfile.nix
new file mode 100644
index 0000000000000..25c0c5bd866d3
--- /dev/null
+++ b/nixos/tests/systemd-initrd-luks-keyfile.nix
@@ -0,0 +1,53 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: let
+
+  keyfile = pkgs.writeText "luks-keyfile" ''
+    MIGHAoGBAJ4rGTSo/ldyjQypd0kuS7k2OSsmQYzMH6TNj3nQ/vIUjDn7fqa3slt2
+    gV6EK3TmTbGc4tzC1v4SWx2m+2Bjdtn4Fs4wiBwn1lbRdC6i5ZYCqasTWIntWn+6
+    FllUkMD5oqjOR/YcboxG8Z3B5sJuvTP9llsF+gnuveWih9dpbBr7AgEC
+  '';
+
+in {
+  name = "systemd-initrd-luks-keyfile";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+    boot.initrd.systemd = {
+      enable = true;
+      emergencyAccess = true;
+    };
+
+    specialisation.boot-luks.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        cryptroot = {
+          device = "/dev/vdc";
+          keyFile = "/etc/cryptroot.key";
+        };
+      };
+      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+      boot.initrd.secrets."/etc/cryptroot.key" = keyfile;
+    };
+  };
+
+  testScript = ''
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("cryptsetup luksFormat -q --iter-time=1 -d ${keyfile} /dev/vdc")
+
+    # Boot from the encrypted disk
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Boot and decrypt the disk
+    machine.wait_for_unit("multi-user.target")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-luks-password.nix b/nixos/tests/systemd-initrd-luks-password.nix
new file mode 100644
index 0000000000000..e8e651f7b35f8
--- /dev/null
+++ b/nixos/tests/systemd-initrd-luks-password.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-luks-password";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+    boot.initrd.systemd = {
+      enable = true;
+      emergencyAccess = true;
+    };
+
+    specialisation.boot-luks.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        # We have two disks and only type one password - key reuse is in place
+        cryptroot.device = "/dev/vdc";
+        cryptroot2.device = "/dev/vdd";
+      };
+      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+    };
+  };
+
+  testScript = ''
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdd -")
+
+    # Boot from the encrypted disk
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Boot and decrypt the disk
+    machine.start()
+    machine.wait_for_console_text("Please enter passphrase for disk cryptroot")
+    machine.send_console("supersecret\n")
+    machine.wait_for_unit("multi-user.target")
+
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-simple.nix b/nixos/tests/systemd-initrd-simple.nix
new file mode 100644
index 0000000000000..5d98114304b75
--- /dev/null
+++ b/nixos/tests/systemd-initrd-simple.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-simple";
+
+  nodes.machine = { pkgs, ... }: {
+    boot.initrd.systemd = {
+      enable = true;
+      emergencyAccess = true;
+    };
+    fileSystems = lib.mkVMOverride {
+      "/".autoResize = true;
+    };
+  };
+
+  testScript = ''
+    import subprocess
+
+    with subtest("handover to stage-2 systemd works"):
+        machine.wait_for_unit("multi-user.target")
+        machine.succeed("systemd-analyze | grep -q '(initrd)'")  # direct handover
+        machine.succeed("touch /testfile")  # / is writable
+        machine.fail("touch /nix/store/testfile")  # /nix/store is not writable
+        # Special filesystems are mounted by systemd
+        machine.succeed("[ -e /run/booted-system ]") # /run
+        machine.succeed("[ -e /sys/class ]") # /sys
+        machine.succeed("[ -e /dev/null ]") # /dev
+        machine.succeed("[ -e /proc/1 ]") # /proc
+        # stage-2-init mounted more special filesystems
+        machine.succeed("[ -e /dev/shm ]") # /dev/shm
+        machine.succeed("[ -e /dev/pts/ptmx ]") # /dev/pts
+        machine.succeed("[ -e /run/keys ]") # /run/keys
+
+
+    with subtest("growfs works"):
+        oldAvail = machine.succeed("df --output=avail / | sed 1d")
+        machine.shutdown()
+
+        subprocess.check_call(["qemu-img", "resize", "vm-state-machine/machine.qcow2", "+1G"])
+
+        machine.start()
+        newAvail = machine.succeed("df --output=avail / | sed 1d")
+
+        assert int(oldAvail) < int(newAvail), "File system did not grow"
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-swraid.nix b/nixos/tests/systemd-initrd-swraid.nix
new file mode 100644
index 0000000000000..28a0fb3192aed
--- /dev/null
+++ b/nixos/tests/systemd-initrd-swraid.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-swraid";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+
+    environment.systemPackages = with pkgs; [ mdadm e2fsprogs ]; # for mdadm and mkfs.ext4
+    boot.initrd = {
+      systemd = {
+        enable = true;
+        emergencyAccess = true;
+      };
+      services.swraid = {
+        enable = true;
+        mdadmConf = ''
+          ARRAY /dev/md0 devices=/dev/vdc,/dev/vdd
+        '';
+      };
+      kernelModules = [ "raid0" ];
+    };
+
+    specialisation.boot-swraid.configuration.virtualisation.bootDevice = "/dev/disk/by-label/testraid";
+  };
+
+  testScript = ''
+    # Create RAID
+    machine.succeed("mdadm --create --force /dev/md0 -n 2 --level=raid0 /dev/vdc /dev/vdd")
+    machine.succeed("mkfs.ext4 -L testraid /dev/md0")
+    machine.succeed("mkdir -p /mnt && mount /dev/md0 /mnt && echo hello > /mnt/test && umount /mnt")
+
+    # Boot from the RAID
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-swraid.conf")
+    machine.succeed("sync")
+    machine.crash()
+    machine.wait_for_unit("multi-user.target")
+
+    # Ensure we have successfully booted from the RAID
+    assert "(initrd)" in machine.succeed("systemd-analyze")  # booted with systemd in stage 1
+    assert "/dev/md0 on / type ext4" in machine.succeed("mount")
+    assert "hello" in machine.succeed("cat /test")
+    assert "md0" in machine.succeed("cat /proc/mdstat")
+  '';
+})
diff --git a/nixos/tests/systemd-journal.nix b/nixos/tests/systemd-journal.nix
index 6ab7c72463181..d2063a3b9a44e 100644
--- a/nixos/tests/systemd-journal.nix
+++ b/nixos/tests/systemd-journal.nix
@@ -6,7 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     maintainers = [ lewo ];
   };
 
-  machine = { pkgs, lib, ... }: {
+  nodes.machine = { pkgs, lib, ... }: {
     services.journald.enableHttpGateway = true;
   };
 
diff --git a/nixos/tests/systemd-machinectl.nix b/nixos/tests/systemd-machinectl.nix
new file mode 100644
index 0000000000000..ce0c56a360e96
--- /dev/null
+++ b/nixos/tests/systemd-machinectl.nix
@@ -0,0 +1,85 @@
+import ./make-test-python.nix (
+  let
+
+    container = {
+      # We re-use the NixOS container option ...
+      boot.isContainer = true;
+      # ... and revert unwanted defaults
+      networking.useHostResolvConf = false;
+
+      # use networkd to obtain systemd network setup
+      networking.useNetworkd = true;
+      networking.useDHCP = false;
+
+      # systemd-nspawn expects /sbin/init
+      boot.loader.initScript.enable = true;
+
+      imports = [ ../modules/profiles/minimal.nix ];
+    };
+
+    containerSystem = (import ../lib/eval-config.nix {
+      modules = [ container ];
+    }).config.system.build.toplevel;
+
+    containerName = "container";
+    containerRoot = "/var/lib/machines/${containerName}";
+
+  in
+  {
+    name = "systemd-machinectl";
+
+    nodes.machine = { lib, ... }: {
+      # use networkd to obtain systemd network setup
+      networking.useNetworkd = true;
+      networking.useDHCP = false;
+      services.resolved.enable = false;
+
+      # open DHCP server on interface to container
+      networking.firewall.trustedInterfaces = [ "ve-+" ];
+
+      # do not try to access cache.nixos.org
+      nix.settings.substituters = lib.mkForce [ ];
+
+      virtualisation.additionalPaths = [ containerSystem ];
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("default.target");
+
+      # Install container
+      machine.succeed("mkdir -p ${containerRoot}");
+      # Workaround for nixos-install
+      machine.succeed("chmod o+rx /var/lib/machines");
+      machine.succeed("nixos-install --root ${containerRoot} --system ${containerSystem} --no-channel-copy --no-root-passwd");
+
+      # Allow systemd-nspawn to apply user namespace on immutable files
+      machine.succeed("chattr -i ${containerRoot}/var/empty");
+
+      # Test machinectl start
+      machine.succeed("machinectl start ${containerName}");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test systemd-nspawn network configuration
+      machine.succeed("ping -n -c 1 ${containerName}");
+
+      # Test systemd-nspawn uses a user namespace
+      machine.succeed("test `stat ${containerRoot}/var/empty -c %u%g` != 00");
+
+      # Test systemd-nspawn reboot
+      machine.succeed("machinectl shell ${containerName} /run/current-system/sw/bin/reboot");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test machinectl reboot
+      machine.succeed("machinectl reboot ${containerName}");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test machinectl stop
+      machine.succeed("machinectl stop ${containerName}");
+
+      # Show to to delete the container
+      machine.succeed("chattr -i ${containerRoot}/var/empty");
+      machine.succeed("rm -rf ${containerRoot}");
+    '';
+  }
+)
diff --git a/nixos/tests/systemd-unit-path.nix b/nixos/tests/systemd-misc.nix
index 5998a187188a1..0ddd51100463e 100644
--- a/nixos/tests/systemd-unit-path.nix
+++ b/nixos/tests/systemd-misc.nix
@@ -29,10 +29,23 @@ let
   };
 in
 {
-  name = "systemd-unit-path";
+  name = "systemd-misc";
 
-  machine = { pkgs, lib, ... }: {
+  nodes.machine = { pkgs, lib, ... }: {
     boot.extraSystemdUnitPaths = [ "/etc/systemd-rw/system" ];
+
+    users.users.limited = {
+      isNormalUser = true;
+      uid = 1000;
+    };
+
+    systemd.units."user-1000.slice.d/limits.conf" = {
+      text = ''
+        [Slice]
+        TasksAccounting=yes
+        TasksMax=100
+      '';
+    };
   };
 
   testScript = ''
@@ -43,5 +56,7 @@ in
     )
     machine.succeed("systemctl start example.service")
     machine.succeed("systemctl status example.service | grep 'Active: active'")
+
+    machine.succeed("systemctl show --property TasksMax --value user-1000.slice | grep 100")
   '';
 })
diff --git a/nixos/tests/systemd-networkd-vrf.nix b/nixos/tests/systemd-networkd-vrf.nix
index 8a1580fc2ada7..3c18f788e927d 100644
--- a/nixos/tests/systemd-networkd-vrf.nix
+++ b/nixos/tests/systemd-networkd-vrf.nix
@@ -138,18 +138,18 @@ in {
   };
 
   testScript = ''
-    def compare_tables(expected, actual):
-        assert (
-            expected == actual
-        ), """
-        Routing tables don't match!
-        Expected:
-          {}
-        Actual:
-          {}
-        """.format(
-            expected, actual
-        )
+    import json
+
+    def compare(raw_json, to_compare):
+        data = json.loads(raw_json)
+        assert len(raw_json) >= len(to_compare)
+        for i, row in enumerate(to_compare):
+            actual = data[i]
+            assert len(row.keys()) > 0
+            for key, value in row.items():
+                assert value == actual[key], f"""
+                  In entry {i}, value {key}: got: {actual[key]}, expected {value}
+                """
 
 
     start_all()
@@ -159,23 +159,18 @@ in {
     node2.wait_for_unit("network.target")
     node3.wait_for_unit("network.target")
 
-    # NOTE: please keep in mind that the trailing whitespaces in the following strings
-    # are intentional as the output is compared against the raw `iproute2`-output.
-    # editorconfig-checker-disable
     client_ipv4_table = """
-    192.168.1.2 dev vrf1 proto static metric 100 
+    192.168.1.2 dev vrf1 proto static metric 100\x20
     192.168.2.3 dev vrf2 proto static metric 100
     """.strip()
     vrf1_table = """
-    broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.1 
-    192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1 
-    local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1 
+    192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1\x20
+    local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1\x20
     broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.1
     """.strip()
     vrf2_table = """
-    broadcast 192.168.2.0 dev eth2 proto kernel scope link src 192.168.2.1 
-    192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1 
-    local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1 
+    192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1\x20
+    local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1\x20
     broadcast 192.168.2.255 dev eth2 proto kernel scope link src 192.168.2.1
     """.strip()
     # editorconfig-checker-enable
@@ -183,14 +178,28 @@ in {
     # Check that networkd properly configures the main routing table
     # and the routing tables for the VRF.
     with subtest("check vrf routing tables"):
-        compare_tables(
-            client_ipv4_table, client.succeed("ip -4 route list | head -n2").strip()
+        compare(
+            client.succeed("ip --json -4 route list"),
+            [
+                {"dst": "192.168.1.2", "dev": "vrf1", "metric": 100},
+                {"dst": "192.168.2.3", "dev": "vrf2", "metric": 100}
+            ]
         )
-        compare_tables(
-            vrf1_table, client.succeed("ip -4 route list table 23 | head -n4").strip()
+        compare(
+            client.succeed("ip --json -4 route list table 23"),
+            [
+                {"dst": "192.168.1.0/24", "dev": "eth1", "prefsrc": "192.168.1.1"},
+                {"type": "local", "dst": "192.168.1.1", "dev": "eth1", "prefsrc": "192.168.1.1"},
+                {"type": "broadcast", "dev": "eth1", "prefsrc": "192.168.1.1", "dst": "192.168.1.255"}
+            ]
         )
-        compare_tables(
-            vrf2_table, client.succeed("ip -4 route list table 42 | head -n4").strip()
+        compare(
+            client.succeed("ip --json -4 route list table 42"),
+            [
+                {"dst": "192.168.2.0/24", "dev": "eth2", "prefsrc": "192.168.2.1"},
+                {"type": "local", "dst": "192.168.2.1", "dev": "eth2", "prefsrc": "192.168.2.1"},
+                {"type": "broadcast", "dev": "eth2", "prefsrc": "192.168.2.1", "dst": "192.168.2.255"}
+            ]
         )
 
     # Ensure that other nodes are reachable via ICMP through the VRF.
diff --git a/nixos/tests/systemd-networkd.nix b/nixos/tests/systemd-networkd.nix
index 7faeae3704eca..6c423f4140b1f 100644
--- a/nixos/tests/systemd-networkd.nix
+++ b/nixos/tests/systemd-networkd.nix
@@ -8,6 +8,9 @@ let generateNodeConf = { lib, pkgs, config, privk, pubk, peerId, nodeId, ...}: {
       environment.systemPackages = with pkgs; [ wireguard-tools ];
       systemd.network = {
         enable = true;
+        config = {
+          routeTables.custom = 23;
+        };
         netdevs = {
           "90-wg0" = {
             netdevConfig = { Kind = "wireguard"; Name = "wg0"; };
@@ -39,6 +42,7 @@ let generateNodeConf = { lib, pkgs, config, privk, pubk, peerId, nodeId, ...}: {
             address = [ "10.0.0.${nodeId}/32" ];
             routes = [
               { routeConfig = { Gateway = "10.0.0.${nodeId}"; Destination = "10.0.0.0/24"; }; }
+              { routeConfig = { Gateway = "10.0.0.${nodeId}"; Destination = "10.0.0.0/24"; Table = "custom"; }; }
             ];
           };
           "30-eth1" = {
@@ -88,6 +92,12 @@ testScript = ''
     node2.wait_for_unit("systemd-networkd-wait-online.service")
 
     # ================================
+    # Networkd Config
+    # ================================
+    node1.succeed("grep RouteTable=custom:23 /etc/systemd/networkd.conf")
+    node1.succeed("sudo ip route show table custom | grep '10.0.0.0/24 via 10.0.0.1 dev wg0 proto static'")
+
+    # ================================
     # Wireguard
     # ================================
     node1.succeed("ping -c 5 10.0.0.2")
diff --git a/nixos/tests/systemd-nspawn.nix b/nixos/tests/systemd-nspawn.nix
index 5bf55060d2e03..c2cb92d11301c 100644
--- a/nixos/tests/systemd-nspawn.nix
+++ b/nixos/tests/systemd-nspawn.nix
@@ -25,8 +25,15 @@ let
   nspawnImages = (pkgs.runCommand "localhost" { buildInputs = [ pkgs.coreutils pkgs.gnupg ]; } ''
     mkdir -p $out
     cd $out
+
+    # produce a testimage.raw
     dd if=/dev/urandom of=$out/testimage.raw bs=$((1024*1024+7)) count=5
-    sha256sum testimage.raw > SHA256SUMS
+
+    # produce a testimage2.tar.xz, containing the hello store path
+    tar cvJpf testimage2.tar.xz ${pkgs.hello}
+
+    # produce signature(s)
+    sha256sum testimage* > SHA256SUMS
     export GNUPGHOME="$(mktemp -d)"
     cp -R ${gpgKeyring}/* $GNUPGHOME
     gpg --batch --sign --detach-sign --output SHA256SUMS.gpg SHA256SUMS
@@ -56,5 +63,9 @@ in {
     client.succeed(
         "cmp /var/lib/machines/testimage.raw ${nspawnImages}/testimage.raw"
     )
+    client.succeed("machinectl pull-tar --verify=signature http://server/testimage2.tar.xz")
+    client.succeed(
+        "cmp /var/lib/machines/testimage2/${pkgs.hello}/bin/hello ${pkgs.hello}/bin/hello"
+    )
   '';
 })
diff --git a/nixos/tests/systemd-shutdown.nix b/nixos/tests/systemd-shutdown.nix
new file mode 100644
index 0000000000000..688cd6dd2c175
--- /dev/null
+++ b/nixos/tests/systemd-shutdown.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ pkgs, systemdStage1 ? false, ...} : let
+  msg = "Shutting down NixOS";
+in {
+  name = "systemd-shutdown";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ das_j ];
+  };
+
+  nodes.machine = {
+    imports = [ ../modules/profiles/minimal.nix ];
+    systemd.shutdownRamfs.contents."/etc/systemd/system-shutdown/shutdown-message".source = pkgs.writeShellScript "shutdown-message" ''
+      echo "${msg}"
+    '';
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    # .shutdown() would wait for the machine to power off
+    machine.succeed("systemctl poweroff")
+    # Message printed by systemd-shutdown
+    machine.wait_for_console_text("Unmounting '/oldroot'")
+    machine.wait_for_console_text("${msg}")
+    # Don't try to sync filesystems
+    machine.booted = False
+  '';
+})
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
index f86daa5eea974..3317823e03f76 100644
--- a/nixos/tests/systemd.nix
+++ b/nixos/tests/systemd.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "systemd";
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     imports = [ common/user-account.nix common/x11.nix ];
 
     virtualisation.emptyDiskImages = [ 512 512 ];
@@ -192,5 +192,9 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     with subtest("systemd per-unit accounting works"):
         assert "IP traffic received: 84B" in output_ping
         assert "IP traffic sent: 84B" in output_ping
+
+    with subtest("systemd environment is properly set"):
+        machine.systemctl("daemon-reexec")  # Rewrites /proc/1/environ
+        machine.succeed("grep -q TZDIR=/etc/zoneinfo /proc/1/environ")
   '';
 })
diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix
index f34782c7059a9..b2bd421e231f0 100644
--- a/nixos/tests/taskserver.nix
+++ b/nixos/tests/taskserver.nix
@@ -63,6 +63,7 @@ in {
     server = {
       services.taskserver.enable = true;
       services.taskserver.listenHost = "::";
+      services.taskserver.openFirewall = true;
       services.taskserver.fqdn = "server";
       services.taskserver.organisations = {
         testOrganisation.users = [ "alice" "foo" ];
diff --git a/nixos/tests/telegraf.nix b/nixos/tests/telegraf.nix
index d99680ce2c3c4..c3cdb1645213a 100644
--- a/nixos/tests/telegraf.nix
+++ b/nixos/tests/telegraf.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ mic92 ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     services.telegraf.enable = true;
     services.telegraf.environmentFiles = [(pkgs.writeText "secrets" ''
       SECRET=example
diff --git a/nixos/tests/teleport.nix b/nixos/tests/teleport.nix
index 15b16e44409d3..34bf1bc0c70d0 100644
--- a/nixos/tests/teleport.nix
+++ b/nixos/tests/teleport.nix
@@ -72,9 +72,9 @@ in
     nodes = { inherit minimal; };
 
     testScript = ''
-      minimal.wait_for_open_port("3025")
-      minimal.wait_for_open_port("3080")
-      minimal.wait_for_open_port("3022")
+      minimal.wait_for_open_port(3025)
+      minimal.wait_for_open_port(3080)
+      minimal.wait_for_open_port(3022)
     '';
   };
 
@@ -86,12 +86,12 @@ in
 
     testScript = ''
       with subtest("teleport ready"):
-          server.wait_for_open_port("3025")
-          client.wait_for_open_port("3022")
+          server.wait_for_open_port(3025)
+          client.wait_for_open_port(3022)
 
       with subtest("check applied configuration"):
           server.wait_until_succeeds("tctl get nodes --format=json | ${pkgs.jq}/bin/jq -e '.[] | select(.spec.hostname==\"client\") | .metadata.labels.role==\"client\"'")
-          server.wait_for_open_port("3000")
+          server.wait_for_open_port(3000)
           client.succeed("journalctl -u teleport.service --grep='DEBU'")
           server.succeed("journalctl -u teleport.service --grep='Starting teleport in insecure mode.'")
     '';
diff --git a/nixos/tests/terminal-emulators.nix b/nixos/tests/terminal-emulators.nix
new file mode 100644
index 0000000000000..c724608b91554
--- /dev/null
+++ b/nixos/tests/terminal-emulators.nix
@@ -0,0 +1,208 @@
+# Terminal emulators all present a pretty similar interface.
+# That gives us an opportunity to easily test their basic functionality with a single codebase.
+#
+# There are two tests run on each terminal emulator
+# - can it successfully execute a command passed on the cmdline?
+# - can it successfully display a colour?
+# the latter is used as a proxy for "can it display text?", without going through all the intricacies of OCR.
+#
+# 256-colour terminal mode is used to display the test colour, since it has a universally-applicable palette (unlike 8- and 16- colour, where the colours are implementation-defined), and it is widely supported (unlike 24-bit colour).
+#
+# Future work:
+# - Wayland support (both for testing the existing terminals, and for testing wayland-only terminals like foot and havoc)
+# - Test keyboard input? (skipped for now, to eliminate the possibility of race conditions and focus issues)
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let tests = {
+      alacritty.pkg = p: p.alacritty;
+
+      contour.pkg = p: p.contour;
+      contour.cmd = "contour $command";
+
+      cool-retro-term.pkg = p: p.cool-retro-term;
+      cool-retro-term.colourTest = false; # broken by gloss effect
+
+      ctx.pkg = p: p.ctx;
+      ctx.pinkValue = "#FE0065";
+
+      darktile.pkg = p: p.darktile;
+
+      eterm.pkg = p: p.eterm;
+      eterm.executable = "Eterm";
+      eterm.pinkValue = "#D40055";
+
+      germinal.pkg = p: p.germinal;
+
+      gnome-terminal.pkg = p: p.gnome.gnome-terminal;
+
+      guake.pkg = p: p.guake;
+      guake.cmd = "SHELL=$command guake --show";
+      guake.kill = true;
+
+      hyper.pkg = p: p.hyper;
+
+      kermit.pkg = p: p.kermit-terminal;
+
+      kgx.pkg = p: p.kgx;
+      kgx.cmd = "kgx -e $command";
+      kgx.kill = true;
+
+      kitty.pkg = p: p.kitty;
+      kitty.cmd = "kitty $command";
+
+      konsole.pkg = p: p.plasma5Packages.konsole;
+
+      lxterminal.pkg = p: p.lxterminal;
+
+      mate-terminal.pkg = p: p.mate.mate-terminal;
+      mate-terminal.cmd = "SHELL=$command mate-terminal --disable-factory"; # factory mode uses dbus, and we don't have a proper dbus session set up
+
+      mlterm.pkg = p: p.mlterm;
+
+      mrxvt.pkg = p: p.mrxvt;
+
+      qterminal.pkg = p: p.lxqt.qterminal;
+      qterminal.kill = true;
+
+      roxterm.pkg = p: p.roxterm;
+      roxterm.cmd = "roxterm -e $command";
+
+      sakura.pkg = p: p.sakura;
+
+      st.pkg = p: p.st;
+      st.kill = true;
+
+      stupidterm.pkg = p: p.stupidterm;
+      stupidterm.cmd = "stupidterm -- $command";
+
+      terminator.pkg = p: p.terminator;
+      terminator.cmd = "terminator -e $command";
+
+      terminology.pkg = p: p.enlightenment.terminology;
+      terminology.cmd = "SHELL=$command terminology --no-wizard=true";
+      terminology.colourTest = false; # broken by gloss effect
+
+      termite.pkg = p: p.termite;
+
+      termonad.pkg = p: p.termonad;
+
+      tilda.pkg = p: p.tilda;
+
+      tilix.pkg = p: p.tilix;
+      tilix.cmd = "tilix -e $command";
+
+      urxvt.pkg = p: p.rxvt-unicode;
+
+      wayst.pkg = p: p.wayst;
+      wayst.pinkValue = "#FF0066";
+
+      wezterm.pkg = p: p.wezterm;
+
+      xfce4-terminal.pkg = p: p.xfce.xfce4-terminal;
+
+      xterm.pkg = p: p.xterm;
+    };
+in mapAttrs (name: { pkg, executable ? name, cmd ? "SHELL=$command ${executable}", colourTest ? true, pinkValue ? "#FF0087", kill ? false }: makeTest
+{
+  name = "terminal-emulator-${name}";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ jjjollyjim ];
+  };
+
+  machine = { pkgsInner, ... }:
+
+  {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+
+    # Hyper (and any other electron-based terminals) won't run as root
+    test-support.displayManager.auto.user = "alice";
+
+    environment.systemPackages = [
+      (pkg pkgs)
+      (pkgs.writeShellScriptBin "report-success" ''
+        echo 1 > /tmp/term-ran-successfully
+        ${optionalString kill "pkill ${executable}"}
+      '')
+      (pkgs.writeShellScriptBin "display-colour" ''
+        # A 256-colour background colour code for pink, then spaces.
+        #
+        # Background is used rather than foreground to minimize the effect of anti-aliasing.
+        #
+        # Keep adding more in case the window is partially offscreen to the left or requires
+        # a change to correctly redraw after initialising the window (as with ctx).
+
+        while :
+        do
+            echo -ne "\e[48;5;198m                   "
+            sleep 0.5
+        done
+        sleep infinity
+      '')
+      (pkgs.writeShellScriptBin "run-in-this-term" "sudo -u alice run-in-this-term-wrapped $1")
+
+      (pkgs.writeShellScriptBin "run-in-this-term-wrapped" "command=\"$(which \"$1\")\"; ${cmd}")
+    ];
+
+    # Helpful reminder to add this test to passthru.tests
+    warnings = if !((pkg pkgs) ? "passthru" && (pkg pkgs).passthru ? "tests") then [ "The package for ${name} doesn't have a passthru.tests" ] else [ ];
+  };
+
+  # We need imagemagick, though not tesseract
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+  in ''
+    with subtest("wait for x"):
+        start_all()
+        machine.wait_for_x()
+
+    with subtest("have the terminal run a command"):
+        # We run this command synchronously, so we can be certain the exit codes are happy
+        machine.${if kill then "execute" else "succeed"}("run-in-this-term report-success")
+        machine.wait_for_file("/tmp/term-ran-successfully")
+    ${optionalString colourTest ''
+
+    import tempfile
+    import subprocess
+
+
+    def check_for_pink(final=False) -> bool:
+        with tempfile.NamedTemporaryFile() as tmpin:
+            machine.send_monitor_command("screendump {}".format(tmpin.name))
+
+            cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format(
+                tmpin.name
+            )
+            ret = subprocess.run(cmd, shell=True, capture_output=True)
+            if ret.returncode != 0:
+                raise Exception(
+                    "image analysis failed with exit code {}".format(ret.returncode)
+                )
+
+            text = ret.stdout.decode("utf-8")
+            return "${pinkValue}" in text
+
+
+    with subtest("ensuring no pink is present without the terminal"):
+        assert (
+            check_for_pink() == False
+        ), "Pink was present on the screen before we even launched a terminal!"
+
+    with subtest("have the terminal display a colour"):
+        # We run this command in the background
+        assert machine.shell is not None
+        machine.shell.send(b"(run-in-this-term display-colour |& systemd-cat -t terminal) &\n")
+
+        with machine.nested("Waiting for the screen to have pink on it:"):
+            retry(check_for_pink)
+  ''}'';
+}
+
+  ) tests
diff --git a/nixos/tests/tinywl.nix b/nixos/tests/tinywl.nix
index b286cab77945c..411cdb1f64192 100644
--- a/nixos/tests/tinywl.nix
+++ b/nixos/tests/tinywl.nix
@@ -6,10 +6,11 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
       maintainers = with lib.maintainers; [ primeos ];
     };
 
-    machine = { config, ... }: {
+    nodes.machine = { config, ... }: {
       # Automatically login on tty1 as a normal user:
       imports = [ ./common/user-account.nix ];
       services.getty.autologinUser = "alice";
+      security.polkit.enable = true;
 
       environment = {
         systemPackages = with pkgs; [ tinywl foot wayland-utils ];
diff --git a/nixos/tests/tomcat.nix b/nixos/tests/tomcat.nix
new file mode 100644
index 0000000000000..4cfb3cc5a7d84
--- /dev/null
+++ b/nixos/tests/tomcat.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "tomcat";
+
+  nodes.machine = { pkgs, ... }: {
+    services.tomcat.enable = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("tomcat.service")
+    machine.wait_for_open_port(8080)
+    machine.wait_for_file("/var/tomcat/webapps/examples");
+    machine.succeed(
+        "curl --fail http://localhost:8080/examples/servlets/servlet/HelloWorldExample | grep 'Hello World!'"
+    )
+    machine.succeed(
+        "curl --fail http://localhost:8080/examples/jsp/jsp2/simpletag/hello.jsp | grep 'Hello, world!'"
+    )
+  '';
+})
diff --git a/nixos/tests/tor.nix b/nixos/tests/tor.nix
index c061f59226cfb..71ec9df4641fa 100644
--- a/nixos/tests/tor.nix
+++ b/nixos/tests/tor.nix
@@ -1,24 +1,19 @@
 import ./make-test-python.nix ({ lib, ... }: with lib;
 
-rec {
+{
   name = "tor";
   meta.maintainers = with maintainers; [ joachifm ];
 
-  common =
-    { ... }:
-    { boot.kernelParams = [ "audit=0" "apparmor=0" "quiet" ];
-      networking.firewall.enable = false;
-      networking.useDHCP = false;
-    };
+  nodes.client = { pkgs, ... }: {
+    boot.kernelParams = [ "audit=0" "apparmor=0" "quiet" ];
+    networking.firewall.enable = false;
+    networking.useDHCP = false;
 
-  nodes.client =
-    { pkgs, ... }:
-    { imports = [ common ];
-      environment.systemPackages = with pkgs; [ netcat ];
-      services.tor.enable = true;
-      services.tor.client.enable = true;
-      services.tor.settings.ControlPort = 9051;
-    };
+    environment.systemPackages = with pkgs; [ netcat ];
+    services.tor.enable = true;
+    services.tor.client.enable = true;
+    services.tor.settings.ControlPort = 9051;
+  };
 
   testScript = ''
     client.wait_for_unit("tor.service")
diff --git a/nixos/tests/traefik.nix b/nixos/tests/traefik.nix
index 1d6c0a479ef62..989ec390c0603 100644
--- a/nixos/tests/traefik.nix
+++ b/nixos/tests/traefik.nix
@@ -11,14 +11,20 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       environment.systemPackages = [ pkgs.curl ];
     };
     traefik = { config, pkgs, ... }: {
-      virtualisation.oci-containers.containers.nginx = {
-        extraOptions = [
-          "-l" "traefik.enable=true"
-          "-l" "traefik.http.routers.nginx.entrypoints=web"
-          "-l" "traefik.http.routers.nginx.rule=Host(`nginx.traefik.test`)"
-        ];
-        image = "nginx-container";
-        imageFile = pkgs.dockerTools.examples.nginx;
+      virtualisation.oci-containers = {
+        backend = "docker";
+        containers.nginx = {
+          extraOptions = [
+            "-l"
+            "traefik.enable=true"
+            "-l"
+            "traefik.http.routers.nginx.entrypoints=web"
+            "-l"
+            "traefik.http.routers.nginx.rule=Host(`nginx.traefik.test`)"
+          ];
+          image = "nginx-container";
+          imageFile = pkgs.dockerTools.examples.nginx;
+        };
       };
 
       networking.firewall.allowedTCPPorts = [ 80 ];
diff --git a/nixos/tests/transmission.nix b/nixos/tests/transmission.nix
index 7e2648804de21..b69ddd84d009a 100644
--- a/nixos/tests/transmission.nix
+++ b/nixos/tests/transmission.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ coconnor ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
 
     networking.firewall.allowedTCPPorts = [ 9091 ];
diff --git a/nixos/tests/tsm-client-gui.nix b/nixos/tests/tsm-client-gui.nix
index e4bcd344a8957..e11501da53d0c 100644
--- a/nixos/tests/tsm-client-gui.nix
+++ b/nixos/tests/tsm-client-gui.nix
@@ -10,7 +10,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
 
   enableOCR = true;
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ./common/x11.nix ];
     programs.tsmClient = {
       enable = true;
diff --git a/nixos/tests/tuptime.nix b/nixos/tests/tuptime.nix
index 6d37e30698390..93410de7bdf52 100644
--- a/nixos/tests/tuptime.nix
+++ b/nixos/tests/tuptime.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ evils ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
     services.tuptime.enable = true;
   };
diff --git a/nixos/tests/turbovnc-headless-server.nix b/nixos/tests/turbovnc-headless-server.nix
index 7d705c56ecf31..1dbf9297c8131 100644
--- a/nixos/tests/turbovnc-headless-server.nix
+++ b/nixos/tests/turbovnc-headless-server.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     maintainers = with lib.maintainers; [ nh2 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
 
     environment.systemPackages = with pkgs; [
       glxinfo
diff --git a/nixos/tests/tuxguitar.nix b/nixos/tests/tuxguitar.nix
index 63a7b6c7dec9b..037f489e54483 100644
--- a/nixos/tests/tuxguitar.nix
+++ b/nixos/tests/tuxguitar.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ asbachb ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
diff --git a/nixos/tests/udisks2.nix b/nixos/tests/udisks2.nix
index 6c4b71aaa2eda..6afb200f85664 100644
--- a/nixos/tests/udisks2.nix
+++ b/nixos/tests/udisks2.nix
@@ -15,7 +15,7 @@ in
     maintainers = [ eelco ];
   };
 
-  machine =
+  nodes.machine =
     { ... }:
     { services.udisks2.enable = true;
       imports = [ ./common/user-account.nix ];
diff --git a/nixos/tests/uptermd.nix b/nixos/tests/uptermd.nix
new file mode 100644
index 0000000000000..429e3c9dd5ff3
--- /dev/null
+++ b/nixos/tests/uptermd.nix
@@ -0,0 +1,65 @@
+import ./make-test-python.nix ({ pkgs, ...}:
+
+let
+  client = {pkgs, ...}:{
+    environment.systemPackages = [ pkgs.upterm ];
+  };
+in
+{
+  name = "uptermd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fleaz ];
+  };
+
+  nodes = {
+    server = {config, ...}: {
+      services.uptermd = {
+        enable = true;
+        openFirewall = true;
+        port = 1337;
+      };
+    };
+    client1 = client;
+    client2 = client;
+  };
+
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("uptermd.service")
+    server.wait_for_unit("network-online.target")
+
+    # wait for upterm port to be reachable
+    client1.wait_until_succeeds("nc -z -v server 1337")
+
+    # Add SSH hostkeys from the server to both clients
+    # uptermd needs an '@cert-authority entry so we need to modify the known_hosts file
+    client1.execute("mkdir -p ~/.ssh && ssh -o StrictHostKeyChecking=no -p 1337 server ls")
+    client1.execute("echo @cert-authority $(cat ~/.ssh/known_hosts) > ~/.ssh/known_hosts")
+    client2.execute("mkdir -p ~/.ssh && ssh -o StrictHostKeyChecking=no -p 1337 server ls")
+    client2.execute("echo @cert-authority $(cat ~/.ssh/known_hosts) > ~/.ssh/known_hosts")
+
+    client1.wait_for_unit("multi-user.target")
+    client1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+    client1.wait_until_tty_matches("1", "login: ")
+    client1.send_chars("root\n")
+    client1.wait_until_succeeds("pgrep -u root bash")
+
+    client1.execute("ssh-keygen -t ed25519 -N \"\" -f /root/.ssh/id_ed25519")
+    client1.send_chars("TERM=xterm upterm host --server ssh://server:1337 --force-command hostname -- bash > /tmp/session-details\n")
+    client1.wait_for_file("/tmp/session-details")
+    client1.send_key("q")
+
+    # uptermd can't connect if we don't have a keypair
+    client2.execute("ssh-keygen -t ed25519 -N \"\" -f /root/.ssh/id_ed25519")
+
+    # Grep the ssh connect command from the output of 'upterm host'
+    ssh_command = client1.succeed("grep 'SSH Session' /tmp/session-details | cut -d':' -f2-").strip()
+
+    # Connect with client2. Because we used '--force-command hostname' we should get "client1" as the output
+    output = client2.succeed(ssh_command)
+
+    assert output.strip() == "client1"
+  '';
+})
diff --git a/nixos/tests/usbguard.nix b/nixos/tests/usbguard.nix
index bb707bdbf7024..d6d3a80c5d23c 100644
--- a/nixos/tests/usbguard.nix
+++ b/nixos/tests/usbguard.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ tnias ];
   };
 
-  machine =
+  nodes.machine =
     { ... }:
     {
       services.usbguard = {
diff --git a/nixos/tests/user-activation-scripts.nix b/nixos/tests/user-activation-scripts.nix
index 0de8664c5ef07..5df072ce0508a 100644
--- a/nixos/tests/user-activation-scripts.nix
+++ b/nixos/tests/user-activation-scripts.nix
@@ -2,7 +2,7 @@ import ./make-test-python.nix ({ lib, ... }: {
   name = "user-activation-scripts";
   meta = with lib.maintainers; { maintainers = [ chkno ]; };
 
-  machine = {
+  nodes.machine = {
     system.userActivationScripts.foo = "mktemp ~/user-activation-ran.XXXXXX";
     users.users.alice = {
       initialPassword = "pass1";
@@ -19,9 +19,9 @@ import ./make-test-python.nix ({ lib, ... }: {
 
     machine.wait_for_unit("multi-user.target")
     machine.wait_for_unit("getty@tty1.service")
-    machine.wait_until_tty_matches(1, "login: ")
+    machine.wait_until_tty_matches("1", "login: ")
     machine.send_chars("alice\n")
-    machine.wait_until_tty_matches(1, "Password: ")
+    machine.wait_until_tty_matches("1", "Password: ")
     machine.send_chars("pass1\n")
     machine.send_chars("touch login-ok\n")
     machine.wait_for_file("/home/alice/login-ok")
diff --git a/nixos/tests/user-home-mode.nix b/nixos/tests/user-home-mode.nix
new file mode 100644
index 0000000000000..070cb0b75cc9d
--- /dev/null
+++ b/nixos/tests/user-home-mode.nix
@@ -0,0 +1,27 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "user-home-mode";
+  meta = with lib.maintainers; { maintainers = [ fbeffa ]; };
+
+  nodes.machine = {
+    users.users.alice = {
+      initialPassword = "pass1";
+      isNormalUser = true;
+    };
+    users.users.bob = {
+      initialPassword = "pass2";
+      isNormalUser = true;
+      homeMode = "750";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("getty@tty1.service")
+    machine.wait_until_tty_matches("1", "login: ")
+    machine.send_chars("alice\n")
+    machine.wait_until_tty_matches("1", "Password: ")
+    machine.send_chars("pass1\n")
+    machine.succeed('[ "$(stat -c %a /home/alice)" == "700" ]')
+    machine.succeed('[ "$(stat -c %a /home/bob)" == "750" ]')
+  '';
+})
diff --git a/nixos/tests/uwsgi.nix b/nixos/tests/uwsgi.nix
index 80dcde324aad7..62da9e0a7168c 100644
--- a/nixos/tests/uwsgi.nix
+++ b/nixos/tests/uwsgi.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     maintainers = [ lnl7 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     users.users.hello  =
       { isSystemUser = true;
         group = "hello";
diff --git a/nixos/tests/v2ray.nix b/nixos/tests/v2ray.nix
index 4808e149d31ed..fb36ea8557d59 100644
--- a/nixos/tests/v2ray.nix
+++ b/nixos/tests/v2ray.nix
@@ -57,7 +57,7 @@ in {
   meta = with lib.maintainers; {
     maintainers = [ servalcatty ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.curl ];
     services.v2ray = {
       enable = true;
diff --git a/nixos/tests/vault-dev.nix b/nixos/tests/vault-dev.nix
new file mode 100644
index 0000000000000..ba9a1015cc13c
--- /dev/null
+++ b/nixos/tests/vault-dev.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "vault-dev";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 mic92 ];
+  };
+  nodes.machine = { pkgs, config, ... }: {
+    environment.systemPackages = [ pkgs.vault ];
+    environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
+    environment.variables.VAULT_TOKEN = "phony-secret";
+
+    services.vault = {
+      enable = true;
+      dev = true;
+      devRootTokenID = config.environment.variables.VAULT_TOKEN;
+    };
+  };
+
+  testScript = ''
+    import json
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("vault.service")
+    machine.wait_for_open_port(8200)
+    out = machine.succeed("vault status -format=json")
+    print(out)
+    status = json.loads(out)
+    assert status.get("initialized") == True
+    machine.succeed("vault kv put secret/foo bar=baz")
+    out = machine.succeed("vault kv get -format=json secret/foo")
+    print(out)
+    status = json.loads(out)
+    assert status.get("data", {}).get("data", {}).get("bar") == "baz"
+  '';
+})
diff --git a/nixos/tests/vault-postgresql.nix b/nixos/tests/vault-postgresql.nix
index 2847af13cbf05..e0e5881c6da79 100644
--- a/nixos/tests/vault-postgresql.nix
+++ b/nixos/tests/vault-postgresql.nix
@@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 roberth ];
   };
-  machine = { lib, pkgs, ... }: {
+  nodes.machine = { lib, pkgs, ... }: {
     environment.systemPackages = [ pkgs.vault ];
     environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
     services.vault.enable = true;
diff --git a/nixos/tests/vault.nix b/nixos/tests/vault.nix
index e86acd5b593fb..1b0a26a4487f0 100644
--- a/nixos/tests/vault.nix
+++ b/nixos/tests/vault.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
   meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 ];
   };
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     environment.systemPackages = [ pkgs.vault ];
     environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
     services.vault.enable = true;
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
index 56f1d245d5052..814d8d7c0ab3e 100644
--- a/nixos/tests/vaultwarden.nix
+++ b/nixos/tests/vaultwarden.nix
@@ -113,7 +113,6 @@ let
                   driver.find_element_by_css_selector('input#masterPasswordRetype').send_keys(
                     '${userPassword}'
                   )
-                  driver.find_element_by_css_selector('input#acceptPolicies').click()
 
                   driver.find_element_by_xpath("//button[contains(., 'Submit')]").click()
 
diff --git a/nixos/tests/vector.nix b/nixos/tests/vector.nix
index 583e60ddc5681..ecf94e33ff17e 100644
--- a/nixos/tests/vector.nix
+++ b/nixos/tests/vector.nix
@@ -9,7 +9,7 @@ with pkgs.lib;
     name = "vector-test1";
     meta.maintainers = [ pkgs.lib.maintainers.happysalada ];
 
-    machine = { config, pkgs, ... }: {
+    nodes.machine = { config, pkgs, ... }: {
       services.vector = {
         enable = true;
         journaldAccess = true;
diff --git a/nixos/tests/vengi-tools.nix b/nixos/tests/vengi-tools.nix
index 6b90542887d51..5bc8d72c77237 100644
--- a/nixos/tests/vengi-tools.nix
+++ b/nixos/tests/vengi-tools.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     maintainers = [ fgaz ];
   };
 
-  machine = { config, pkgs, ... }: {
+  nodes.machine = { config, pkgs, ... }: {
     imports = [
       ./common/x11.nix
     ];
@@ -23,7 +23,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       # OCR on voxedit's window is very expensive, so we avoid wasting a try
       # by letting the window load fully first
       machine.sleep(15)
-      machine.wait_for_text("Palette")
+      machine.wait_for_text("Solid")
       machine.screenshot("screen")
     '';
 })
diff --git a/nixos/tests/vikunja.nix b/nixos/tests/vikunja.nix
index bd884b37f4f91..2f6c4c1f46617 100644
--- a/nixos/tests/vikunja.nix
+++ b/nixos/tests/vikunja.nix
@@ -1,9 +1,7 @@
 import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "vikunja";
 
-  meta = with lib.maintainers; {
-    maintainers = [ em0lar ];
-  };
+  meta.maintainers = with lib.maintainers; [ leona ];
 
   nodes = {
     vikunjaSqlite = { ... }: {
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index f15412d365fa9..1c1b0dac7f37c 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -3,18 +3,9 @@
   pkgs ? import ../.. { inherit system config; },
   debug ? false,
   enableUnfree ? false,
-  # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests)
-  # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs)
-  #   boot.extraModprobeConfig = "options kvm-intel nested=Y";
-  # Without this VirtualBox will use SW virtualization and will only be able
-  # to run 32-bit guests.
-  useKvmNestedVirt ? false,
-  # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM.
-  use64bitGuest ? false
+  use64bitGuest ? true
 }:
 
-assert use64bitGuest -> useKvmNestedVirt;
-
 with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
@@ -26,7 +17,8 @@ let
       #!${pkgs.runtimeShell} -xe
       export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}"
 
-      mkdir -p /run/dbus
+      mkdir -p /run/dbus /var
+      ln -s /run /var
       cat > /etc/passwd <<EOF
       root:x:0:0::/root:/bin/false
       messagebus:x:1:1::/run/dbus:/bin/false
@@ -200,6 +192,7 @@ let
       systemd.services."vboxtestlog-${name}@" = {
         description = "VirtualBox Test Machine Log For ${name}";
         serviceConfig.StandardInput = "socket";
+        serviceConfig.StandardOutput = "journal";
         serviceConfig.SyslogIdentifier = "GUEST-${name}";
         serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
       };
@@ -222,10 +215,11 @@ let
               machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
           machine.succeed("rm -rf ${sharePath}")
           machine.succeed("mkdir -p ${sharePath}")
-          machine.succeed("chown alice.users ${sharePath}")
+          machine.succeed("chown alice:users ${sharePath}")
 
 
       def create_vm_${name}():
+          cleanup_${name}()
           vbm("createvm --name ${name} ${createFlags}")
           vbm("modifyvm ${name} ${vmFlags}")
           vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
@@ -233,7 +227,6 @@ let
           vbm("storageattach ${name} ${diskFlags}")
           vbm("sharedfolder add ${name} ${sharedFlags}")
           vbm("sharedfolder add ${name} ${nixstoreFlags}")
-          cleanup_${name}()
 
           ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
 
@@ -317,10 +310,7 @@ let
   ];
 
   dhcpScript = pkgs: ''
-    ${pkgs.dhcp}/bin/dhclient \
-      -lf /run/dhcp.leases \
-      -pf /run/dhclient.pid \
-      -v eth0 eth1
+    ${pkgs.dhcpcd}/bin/dhcpcd eth0 eth1
 
     otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
     ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
@@ -353,14 +343,13 @@ let
   mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest {
     name = "virtualbox-${name}";
 
-    machine = { lib, config, ... }: {
+    nodes.machine = { lib, config, ... }: {
       imports = let
         mkVMConf = name: val: val.machine // { key = "${name}-config"; };
         vmConfigs = mapAttrsToList mkVMConf vms;
       in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
       virtualisation.memorySize = 2048;
-      virtualisation.qemu.options =
-        if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else [];
+      virtualisation.qemu.options = ["-cpu" "kvm64,svm=on,vmx=on"];
       virtualisation.virtualbox.host.enable = true;
       test-support.displayManager.auto.user = "alice";
       users.users.alice.extraGroups = let
@@ -468,7 +457,7 @@ in mapAttrs (mkVBoxTest false vboxVMs) {
 
   headless = ''
     create_vm_headless()
-    machine.succeed(ru("VBoxHeadless --startvm headless & disown %1"))
+    machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
     wait_for_startup_headless()
     wait_for_vm_boot_headless()
     shutdown_vm_headless()
@@ -476,6 +465,8 @@ in mapAttrs (mkVBoxTest false vboxVMs) {
   '';
 
   host-usb-permissions = ''
+    import sys
+
     user_usb = remove_uuids(vbm("list usbhost"))
     print(user_usb, file=sys.stderr)
     root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
diff --git a/nixos/tests/vscodium.nix b/nixos/tests/vscodium.nix
index 688ddfe07e3e1..3bdb99947a40b 100644
--- a/nixos/tests/vscodium.nix
+++ b/nixos/tests/vscodium.nix
@@ -32,6 +32,14 @@ let
         maintainers = [ synthetica turion ];
       };
       enableOCR = true;
+
+      # testScriptWithTypes:55: error: Item "function" of
+      # "Union[Callable[[Callable[..., Any]], ContextManager[Any]], ContextManager[Any]]"
+      # has no attribute "__enter__"
+      #     with codium_running:
+      #          ^
+      skipTypeCheck = true;
+
       testScript = ''
         @polling_condition
         def codium_running():
diff --git a/nixos/tests/vsftpd.nix b/nixos/tests/vsftpd.nix
new file mode 100644
index 0000000000000..6eaf32b225838
--- /dev/null
+++ b/nixos/tests/vsftpd.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "vsftpd";
+
+  nodes = {
+    server = {
+      services.vsftpd = {
+        enable = true;
+        userlistDeny = false;
+        localUsers = true;
+        userlist = [ "ftp-test-user" ];
+        writeEnable = true;
+        localRoot = "/tmp";
+      };
+      networking.firewall.enable = false;
+
+      users = {
+        users.ftp-test-user = {
+          isSystemUser = true;
+          password = "ftp-test-password";
+          group = "ftp-test-group";
+        };
+        groups.ftp-test-group = {};
+      };
+    };
+
+    client = {};
+  };
+
+  testScript = ''
+    client.start()
+    server.wait_for_unit("vsftpd")
+    server.wait_for_open_port(21)
+
+    client.succeed("curl -u ftp-test-user:ftp-test-password ftp://server")
+    client.succeed('echo "this is a test" > /tmp/test.file.up')
+    client.succeed("curl -v -T /tmp/test.file.up -u ftp-test-user:ftp-test-password ftp://server")
+    client.succeed("curl -u ftp-test-user:ftp-test-password ftp://server/test.file.up > /tmp/test.file.down")
+    client.succeed("diff /tmp/test.file.up /tmp/test.file.down")
+    assert client.succeed("cat /tmp/test.file.up") == server.succeed("cat /tmp/test.file.up")
+    assert client.succeed("cat /tmp/test.file.down") == server.succeed("cat /tmp/test.file.up")
+  '';
+})
diff --git a/nixos/tests/web-apps/healthchecks.nix b/nixos/tests/web-apps/healthchecks.nix
new file mode 100644
index 0000000000000..41374f5e314bb
--- /dev/null
+++ b/nixos/tests/web-apps/healthchecks.nix
@@ -0,0 +1,42 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "healthchecks";
+
+  meta = with lib.maintainers; {
+    maintainers = [ phaer ];
+  };
+
+  nodes.machine = { ... }: {
+    services.healthchecks = {
+      enable = true;
+      settings = {
+        SITE_NAME = "MyUniqueInstance";
+        COMPRESS_ENABLED = "True";
+        SECRET_KEY_FILE = pkgs.writeText "secret"
+          "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("healthchecks.target")
+    machine.wait_until_succeeds("journalctl --since -1m --unit healthchecks --grep Listening")
+
+    with subtest("Home screen loads"):
+        machine.succeed(
+            "curl -sSfL http://localhost:8000 | grep '<title>Sign In'"
+        )
+
+    with subtest("Setting SITE_NAME via freeform option works"):
+        machine.succeed(
+            "curl -sSfL http://localhost:8000 | grep 'MyUniqueInstance</title>'"
+        )
+
+    with subtest("Manage script works"):
+        # Should fail if not called by healthchecks user
+        machine.fail("echo 'print(\"foo\")' | healthchecks-manage help")
+
+        # "shell" sucommand should succeed, needs python in PATH.
+        assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | sudo -u healthchecks healthchecks-manage shell")
+  '';
+})
diff --git a/nixos/tests/web-apps/mastodon.nix b/nixos/tests/web-apps/mastodon.nix
new file mode 100644
index 0000000000000..279a1c59169fa
--- /dev/null
+++ b/nixos/tests/web-apps/mastodon.nix
@@ -0,0 +1,170 @@
+import ../make-test-python.nix ({pkgs, ...}:
+let
+  test-certificates = pkgs.runCommandLocal "test-certificates" { } ''
+    mkdir -p $out
+    echo insecure-root-password > $out/root-password-file
+    echo insecure-intermediate-password > $out/intermediate-password-file
+    ${pkgs.step-cli}/bin/step certificate create "Example Root CA" $out/root_ca.crt $out/root_ca.key --password-file=$out/root-password-file --profile root-ca
+    ${pkgs.step-cli}/bin/step certificate create "Example Intermediate CA 1" $out/intermediate_ca.crt $out/intermediate_ca.key --password-file=$out/intermediate-password-file --ca-password-file=$out/root-password-file --profile intermediate-ca --ca $out/root_ca.crt --ca-key $out/root_ca.key
+  '';
+
+  hosts = ''
+    192.168.2.10 ca.local
+    192.168.2.11 mastodon.local
+  '';
+
+in
+{
+  name = "mastodon";
+  meta.maintainers = with pkgs.lib.maintainers; [ erictapen izorkin ];
+
+  nodes = {
+    ca = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.10"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+      };
+      services.step-ca = {
+        enable = true;
+        address = "0.0.0.0";
+        port = 8443;
+        openFirewall = true;
+        intermediatePasswordFile = "${test-certificates}/intermediate-password-file";
+        settings = {
+          dnsNames = [ "ca.local" ];
+          root = "${test-certificates}/root_ca.crt";
+          crt = "${test-certificates}/intermediate_ca.crt";
+          key = "${test-certificates}/intermediate_ca.key";
+          db = {
+            type = "badger";
+            dataSource = "/var/lib/step-ca/db";
+          };
+          authority = {
+            provisioners = [
+              {
+                type = "ACME";
+                name = "acme";
+              }
+            ];
+          };
+        };
+      };
+    };
+
+    server = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.11"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+        firewall.allowedTCPPorts = [ 80 443 ];
+      };
+
+      security = {
+        acme = {
+          acceptTerms = true;
+          defaults.server = "https://ca.local:8443/acme/acme/directory";
+          defaults.email = "mastodon@mastodon.local";
+        };
+        pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+      };
+
+      services.redis.servers.mastodon = {
+        enable = true;
+        bind = "127.0.0.1";
+        port = 31637;
+      };
+
+      services.mastodon = {
+        enable = true;
+        configureNginx = true;
+        localDomain = "mastodon.local";
+        enableUnixSocket = false;
+        redis = {
+          createLocally = true;
+          host = "127.0.0.1";
+          port = 31637;
+        };
+        database = {
+          createLocally = true;
+          host = "/run/postgresql";
+          port = 5432;
+        };
+        smtp = {
+          createLocally = false;
+          fromAddress = "mastodon@mastodon.local";
+        };
+        extraConfig = {
+          EMAIL_DOMAIN_ALLOWLIST = "example.com";
+        };
+      };
+    };
+
+    client = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.12"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+      };
+
+      security = {
+        pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    ca.wait_for_unit("step-ca.service")
+    ca.wait_for_open_port(8443)
+
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("redis-mastodon.service")
+    server.wait_for_unit("postgresql.service")
+    server.wait_for_unit("mastodon-sidekiq.service")
+    server.wait_for_unit("mastodon-streaming.service")
+    server.wait_for_unit("mastodon-web.service")
+    server.wait_for_open_port(55000)
+    server.wait_for_open_port(55001)
+
+    # Check Mastodon version from remote client
+    client.succeed("curl --fail https://mastodon.local/api/v1/instance | jq -r '.version' | grep '${pkgs.mastodon.version}'")
+
+    # Check using admin CLI
+    # Check Mastodon version
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl version' | grep '${pkgs.mastodon.version}'")
+
+    # Manage accounts
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks add example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks list' | grep 'example.com'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks list' | grep 'mastodon.local'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts create alice --email=alice@example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks remove example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts create bob --email=bob@example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts approve bob'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts delete bob'")
+
+    # Manage IP access
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks add 192.168.0.0/16 --severity=no_access'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks export' | grep '192.168.0.0/16'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl p_blocks export' | grep '172.16.0.0/16'")
+    client.fail("curl --fail https://mastodon.local/about")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks remove 192.168.0.0/16'")
+    client.succeed("curl --fail https://mastodon.local/about")
+
+    ca.shutdown()
+    server.shutdown()
+    client.shutdown()
+  '';
+})
diff --git a/nixos/tests/web-apps/netbox.nix b/nixos/tests/web-apps/netbox.nix
new file mode 100644
index 0000000000000..35decdd49e870
--- /dev/null
+++ b/nixos/tests/web-apps/netbox.nix
@@ -0,0 +1,30 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "netbox";
+
+  meta = with lib.maintainers; {
+    maintainers = [ n0emis ];
+  };
+
+  nodes.machine = { ... }: {
+    services.netbox = {
+      enable = true;
+      secretKeyFile = pkgs.writeText "secret" ''
+        abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("netbox.target")
+    machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
+
+    with subtest("Home screen loads"):
+        machine.succeed(
+            "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
+        )
+
+    with subtest("Staticfiles are generated"):
+        machine.succeed("test -e /var/lib/netbox/static/netbox.js")
+  '';
+})
diff --git a/nixos/tests/web-apps/nifi.nix b/nixos/tests/web-apps/nifi.nix
new file mode 100644
index 0000000000000..92f7fa231df3a
--- /dev/null
+++ b/nixos/tests/web-apps/nifi.nix
@@ -0,0 +1,30 @@
+import ../make-test-python.nix ({pkgs, ...}:
+{
+  name = "nifi";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    nifi = { pkgs, ... }: {
+      virtualisation = {
+        memorySize = 2048;
+        diskSize = 4096;
+      };
+      services.nifi = {
+        enable = true;
+        enableHTTPS = false;
+      };
+    };
+  };
+
+  testScript = ''
+    nifi.start()
+
+    nifi.wait_for_unit("nifi.service")
+    nifi.wait_for_open_port(8080)
+
+    # Check if NiFi is running
+    nifi.succeed("curl --fail http://127.0.0.1:8080/nifi/login 2> /dev/null | grep 'NiFi Login'")
+
+    nifi.shutdown()
+  '';
+})
diff --git a/nixos/tests/web-apps/peertube.nix b/nixos/tests/web-apps/peertube.nix
index 38b31f6c3325a..ecc45bff2e2ca 100644
--- a/nixos/tests/web-apps/peertube.nix
+++ b/nixos/tests/web-apps/peertube.nix
@@ -11,7 +11,7 @@ import ../make-test-python.nix ({pkgs, ...}:
             { address = "192.168.2.10"; prefixLength = 24; }
           ];
         };
-        firewall.allowedTCPPorts = [ 5432 6379 ];
+        firewall.allowedTCPPorts = [ 5432 31638 ];
       };
 
       services.postgresql = {
@@ -30,10 +30,11 @@ import ../make-test-python.nix ({pkgs, ...}:
         '';
       };
 
-      services.redis = {
+      services.redis.servers.peertube = {
         enable = true;
         bind = "0.0.0.0";
         requirePass = "turrQfaQwnanGbcsdhxy";
+        port = 31638;
       };
     };
 
@@ -75,6 +76,7 @@ import ../make-test-python.nix ({pkgs, ...}:
 
         redis = {
           host = "192.168.2.10";
+          port = 31638;
           passwordFile = "/etc/peertube/password-redis-db";
         };
 
@@ -109,10 +111,10 @@ import ../make-test-python.nix ({pkgs, ...}:
     start_all()
 
     database.wait_for_unit("postgresql.service")
-    database.wait_for_unit("redis.service")
+    database.wait_for_unit("redis-peertube.service")
 
     database.wait_for_open_port(5432)
-    database.wait_for_open_port(6379)
+    database.wait_for_open_port(31638)
 
     server.wait_for_unit("peertube.service")
     server.wait_for_open_port(9000)
@@ -120,6 +122,9 @@ import ../make-test-python.nix ({pkgs, ...}:
     # Check if PeerTube is running
     client.succeed("curl --fail http://peertube.local:9000/api/v1/config/about | jq -r '.instance.name' | grep 'PeerTube\ Test\ Server'")
 
+    # Check PeerTube CLI version
+    assert "${pkgs.peertube.version}" in server.succeed('su - peertube -s /bin/sh -c "peertube --version"')
+
     client.shutdown()
     server.shutdown()
     database.shutdown()
diff --git a/nixos/tests/web-apps/phylactery.nix b/nixos/tests/web-apps/phylactery.nix
new file mode 100644
index 0000000000000..cf2689d2300d3
--- /dev/null
+++ b/nixos/tests/web-apps/phylactery.nix
@@ -0,0 +1,20 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "phylactery";
+
+  nodes.machine = { ... }: {
+    services.phylactery = rec {
+      enable = true;
+      port = 8080;
+      library = "/tmp";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit('phylactery')
+    machine.wait_for_open_port(8080)
+    machine.wait_until_succeeds('curl localhost:8080')
+  '';
+
+  meta.maintainers = with lib.maintainers; [ McSinyx ];
+})
diff --git a/nixos/tests/web-servers/agate.nix b/nixos/tests/web-servers/agate.nix
new file mode 100644
index 0000000000000..e364e134cfda4
--- /dev/null
+++ b/nixos/tests/web-servers/agate.nix
@@ -0,0 +1,29 @@
+import ../make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "agate";
+    meta = with lib.maintainers; { maintainers = [ jk ]; };
+
+    nodes = {
+      geminiserver = { pkgs, ... }: {
+        services.agate = {
+          enable = true;
+          hostnames = [ "localhost" ];
+          contentDir = pkgs.writeTextDir "index.gmi" ''
+            # Hello NixOS!
+          '';
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      geminiserver.wait_for_unit("agate")
+      geminiserver.wait_for_open_port(1965)
+
+      with subtest("check is serving over gemini"):
+        response = geminiserver.succeed("${pkgs.gmni}/bin/gmni -j once -i -N gemini://localhost:1965")
+        print(response)
+        assert "Hello NixOS!" in response
+    '';
+  }
+)
diff --git a/nixos/tests/web-servers/unit-php.nix b/nixos/tests/web-servers/unit-php.nix
index 00512b506cc2e..f0df371945e5b 100644
--- a/nixos/tests/web-servers/unit-php.nix
+++ b/nixos/tests/web-servers/unit-php.nix
@@ -6,19 +6,19 @@ in {
   name = "unit-php-test";
   meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
 
-  machine = { config, lib, pkgs, ... }: {
+  nodes.machine = { config, lib, pkgs, ... }: {
     services.unit = {
       enable = true;
       config = pkgs.lib.strings.toJSON {
-        listeners."*:9080".application = "php_80";
-        applications.php_80 = {
-          type = "php 8.0";
+        listeners."*:9081".application = "php_81";
+        applications.php_81 = {
+          type = "php 8.1";
           processes = 1;
           user = "testuser";
           group = "testgroup";
           root = "${testdir}/www";
           index = "info.php";
-          options.file = "${pkgs.unit.usedPhp80}/lib/php.ini";
+          options.file = "${pkgs.unit.usedPhp81}/lib/php.ini";
         };
       };
     };
@@ -34,14 +34,19 @@ in {
     };
   };
   testScript = ''
+    machine.start()
+
     machine.wait_for_unit("unit.service")
+    machine.wait_for_open_port(9081)
 
     # Check so we get an evaluated PHP back
-    response = machine.succeed("curl -f -vvv -s http://127.0.0.1:9080/")
-    assert "PHP Version ${pkgs.unit.usedPhp80.version}" in response, "PHP version not detected"
+    response = machine.succeed("curl -f -vvv -s http://127.0.0.1:9081/")
+    assert "PHP Version ${pkgs.unit.usedPhp81.version}" in response, "PHP version not detected"
 
     # Check so we have database and some other extensions loaded
     for ext in ["json", "opcache", "pdo_mysql", "pdo_pgsql", "pdo_sqlite"]:
         assert ext in response, f"Missing {ext} extension"
+
+    machine.shutdown()
   '';
 })
diff --git a/nixos/tests/wiki-js.nix b/nixos/tests/wiki-js.nix
index 783887d2dcaa9..c3541be5d8b52 100644
--- a/nixos/tests/wiki-js.nix
+++ b/nixos/tests/wiki-js.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     maintainers = [ ma27 ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     virtualisation.memorySize = 2048;
     services.wiki-js = {
       enable = true;
diff --git a/nixos/tests/wine.nix b/nixos/tests/wine.nix
index cc449864c7622..8a64c3179c518 100644
--- a/nixos/tests/wine.nix
+++ b/nixos/tests/wine.nix
@@ -3,7 +3,7 @@
 }:
 
 let
-  inherit (pkgs.lib) concatMapStrings listToAttrs;
+  inherit (pkgs.lib) concatMapStrings listToAttrs optionals optionalString;
   inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
 
   hello32 = "${pkgs.pkgsCross.mingw32.hello}/bin/hello.exe";
@@ -15,7 +15,7 @@ let
       inherit name;
       meta = with pkgs.lib.maintainers; { maintainers = [ chkno ]; };
 
-      machine = { pkgs, ... }: {
+      nodes.machine = { pkgs, ... }: {
         environment.systemPackages = [ pkgs."${packageSet}"."${variant}" ];
         virtualisation.diskSize = 800;
       };
@@ -27,6 +27,9 @@ let
               "bash -c 'wine ${exe} 2> >(tee wine-stderr >&2)'"
           )
           assert 'Hello, world!' in greeting
+        ''
+        # only the full version contains Gecko, but the error is not printed reliably in other variants
+        + optionalString (variant == "full") ''
           machine.fail(
               "fgrep 'Could not find Wine Gecko. HTML rendering will be disabled.' wine-stderr"
           )
@@ -37,5 +40,9 @@ let
 
   variants = [ "base" "full" "minimal" "staging" "unstable" "wayland" ];
 
-in listToAttrs (map (makeWineTest "winePackages" [ hello32 ]) variants
-  ++ map (makeWineTest "wineWowPackages" [ hello32 hello64 ]) variants)
+in
+listToAttrs (
+  map (makeWineTest "winePackages" [ hello32 ]) variants
+  ++ optionals pkgs.stdenv.is64bit
+    (map (makeWineTest "wineWowPackages" [ hello32 hello64 ]) variants)
+)
diff --git a/nixos/tests/wireguard/wg-quick.nix b/nixos/tests/wireguard/wg-quick.nix
index 961c2e15c30f9..bc2cba9118889 100644
--- a/nixos/tests/wireguard/wg-quick.nix
+++ b/nixos/tests/wireguard/wg-quick.nix
@@ -29,6 +29,8 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
 
               inherit (wg-snakeoil-keys.peer1) publicKey;
             };
+
+            dns = [ "10.23.42.2" "fc00::2" "wg0" ];
           };
         };
       };
@@ -38,6 +40,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
         ip6 = "fd00::2";
         extraConfig = {
           boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.useNetworkd = true;
           networking.wg-quick.interfaces.wg0 = {
             address = [ "10.23.42.2/32" "fc00::2/128" ];
             inherit (wg-snakeoil-keys.peer1) privateKey;
@@ -49,6 +52,8 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
 
               inherit (wg-snakeoil-keys.peer0) publicKey;
             };
+
+            dns = [ "10.23.42.1" "fc00::1" "wg0" ];
           };
         };
       };
diff --git a/nixos/tests/without-nix.nix b/nixos/tests/without-nix.nix
index 2fc00b04144f9..b21e9f2844f50 100644
--- a/nixos/tests/without-nix.nix
+++ b/nixos/tests/without-nix.nix
@@ -4,14 +4,23 @@ import ./make-test-python.nix ({ lib, ... }: {
     maintainers = [ ericson2314 ];
   };
 
-  nixpkgs.overlays = [
-    (self: super: {
-      nix = throw "don't want to use this";
-    })
-  ];
-
   nodes.machine = { ... }: {
     nix.enable = false;
+    nixpkgs.overlays = [
+      (self: super: {
+        nix = throw "don't want to use pkgs.nix";
+        nixVersions = lib.mapAttrs (k: throw "don't want to use pkgs.nixVersions.${k}") super.nixVersions;
+        # aliases, some deprecated
+        nix_2_3 = throw "don't want to use pkgs.nix_2_3";
+        nix_2_4 = throw "don't want to use pkgs.nix_2_4";
+        nix_2_5 = throw "don't want to use pkgs.nix_2_5";
+        nix_2_6 = throw "don't want to use pkgs.nix_2_6";
+        nixFlakes = throw "don't want to use pkgs.nixFlakes";
+        nixStable = throw "don't want to use pkgs.nixStable";
+        nixUnstable = throw "don't want to use pkgs.nixUnstable";
+        nixStatic = throw "don't want to use pkgs.nixStatic";
+      })
+    ];
   };
 
   testScript = ''
diff --git a/nixos/tests/wmderland.nix b/nixos/tests/wmderland.nix
index 6de0cd9212eea..ebfd443763e1e 100644
--- a/nixos/tests/wmderland.nix
+++ b/nixos/tests/wmderland.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ takagiy ];
   };
 
-  machine = { lib, ... }: {
+  nodes.machine = { lib, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
     test-support.displayManager.auto.user = "alice";
     services.xserver.displayManager.defaultSession = lib.mkForce "none+wmderland";
diff --git a/nixos/tests/wpa_supplicant.nix b/nixos/tests/wpa_supplicant.nix
index 40d934b8e1db2..a05a79e8367d9 100644
--- a/nixos/tests/wpa_supplicant.nix
+++ b/nixos/tests/wpa_supplicant.nix
@@ -5,7 +5,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
     maintainers = [ rnhmjoj ];
   };
 
-  machine = { ... }: {
+  nodes.machine = { ... }: {
     imports = [ ../modules/profiles/minimal.nix ];
 
     # add a virtual wlan interface
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
index 9051deebae76e..31f00f77c40d4 100644
--- a/nixos/tests/xfce.nix
+++ b/nixos/tests/xfce.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "xfce";
 
-  machine =
+  nodes.machine =
     { pkgs, ... }:
 
     {
diff --git a/nixos/tests/xmonad-xdg-autostart.nix b/nixos/tests/xmonad-xdg-autostart.nix
new file mode 100644
index 0000000000000..2577a9ce2ea13
--- /dev/null
+++ b/nixos/tests/xmonad-xdg-autostart.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "xmonad-xdg-autostart";
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
+  nodes.machine = { pkgs, config, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = "none+xmonad";
+    services.xserver.windowManager.xmonad.enable = true;
+    services.xserver.desktopManager.runXdgAutostartIfNone = true;
+
+    environment.systemPackages = [
+      (pkgs.writeTextFile {
+        name = "test-xdg-autostart";
+        destination = "/etc/xdg/autostart/test-xdg-autostart.desktop";
+        text = ''
+          [Desktop Entry]
+          Name=test-xdg-autoatart
+          Type=Application
+          Terminal=false
+          Exec=${pkgs.coreutils}/bin/touch ${config.users.users.alice.home}/xdg-autostart-executed
+        '';
+      })
+    ];
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.config.users.users.alice;
+    in
+    ''
+      machine.wait_for_x()
+      machine.wait_for_file("${user.home}/xdg-autostart-executed")
+    '';
+})
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
index a2fb38e53bd15..ec48c3e112750 100644
--- a/nixos/tests/xmonad.nix
+++ b/nixos/tests/xmonad.nix
@@ -13,7 +13,9 @@ let
     import System.Environment (getArgs)
     import System.FilePath ((</>))
 
-    main = launch $ def { startupHook = startup } `additionalKeysP` myKeys
+    main = do
+      dirs <- getDirectories
+      launch (def { startupHook = startup } `additionalKeysP` myKeys) dirs
 
     startup = isSessionStart >>= \sessInit ->
       spawn "touch /tmp/${name}"
@@ -23,14 +25,15 @@ let
 
     compiledConfig = printf "xmonad-%s-%s" arch os
 
-    compileRestart resume =
-      whenX (recompile True) $
+    compileRestart resume = do
+      dirs <- asks directories
+
+      whenX (recompile dirs True) $
         when resume writeStateToFile
           *> catchIO
             ( do
-                dir <- getXMonadDataDir
                 args <- getArgs
-                executeFile (dir </> compiledConfig) False args Nothing
+                executeFile (cacheDir dirs </> compiledConfig) False args Nothing
             )
   '';
 
@@ -55,7 +58,7 @@ in {
     maintainers = [ nequissimus ivanbrennan ];
   };
 
-  machine = { pkgs, ... }: {
+  nodes.machine = { pkgs, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
     test-support.displayManager.auto.user = "alice";
     services.xserver.displayManager.defaultSession = "none+xmonad";
@@ -94,7 +97,7 @@ in {
 
     # set up the new config
     machine.succeed("mkdir -p ${user.home}/.xmonad")
-    machine.copy_from_host("${newConfig}", "${user.home}/.xmonad/xmonad.hs")
+    machine.copy_from_host("${newConfig}", "${user.home}/.config/xmonad/xmonad.hs")
 
     # recompile xmonad using the new config
     machine.send_key("alt-ctrl-q")
diff --git a/nixos/tests/xmpp/prosody-mysql.nix b/nixos/tests/xmpp/prosody-mysql.nix
new file mode 100644
index 0000000000000..40f3e308a04ea
--- /dev/null
+++ b/nixos/tests/xmpp/prosody-mysql.nix
@@ -0,0 +1,124 @@
+let
+  cert = pkgs: pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=uploads.example.com/CN=conference.example.com' -days 36500
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+  createUsers = pkgs: pkgs.writeScriptBin "create-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Creates and set password for the 2 xmpp test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl register cthon98 example.com nothunter2
+    prosodyctl register azurediamond example.com hunter2
+  '';
+  delUsers = pkgs: pkgs.writeScriptBin "delete-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Deletes the test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl deluser cthon98@example.com
+    prosodyctl deluser azurediamond@example.com
+  '';
+in import ../make-test-python.nix {
+  name = "prosody-mysql";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${nodes.server.config.networking.primaryIPAddress} example.com
+        ${nodes.server.config.networking.primaryIPAddress} conference.example.com
+        ${nodes.server.config.networking.primaryIPAddress} uploads.example.com
+      '';
+      environment.systemPackages = [
+        (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; })
+      ];
+    };
+    server = { config, pkgs, ... }: {
+      nixpkgs.overlays = [
+        (self: super: {
+          prosody = super.prosody.override {
+            withExtraLuaPackages = p: [ p.luadbi-mysql ];
+          };
+        })
+      ];
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${config.networking.primaryIPAddress} example.com
+        ${config.networking.primaryIPAddress} conference.example.com
+        ${config.networking.primaryIPAddress} uploads.example.com
+      '';
+      networking.firewall.enable = false;
+      environment.systemPackages = [
+        (createUsers pkgs)
+        (delUsers pkgs)
+      ];
+      services.prosody = {
+        enable = true;
+        ssl.cert = "${cert pkgs}/cert.pem";
+        ssl.key = "${cert pkgs}/key.pem";
+        virtualHosts.example = {
+          domain = "example.com";
+          enabled = true;
+          ssl.cert = "${cert pkgs}/cert.pem";
+          ssl.key = "${cert pkgs}/key.pem";
+        };
+        muc = [
+          {
+            domain = "conference.example.com";
+          }
+        ];
+        uploadHttp = {
+          domain = "uploads.example.com";
+        };
+        extraConfig = ''
+          storage = "sql"
+          sql = {
+            driver = "MySQL";
+            database = "prosody";
+            host = "mysql";
+            port = 3306;
+            username = "prosody";
+            password = "password123";
+          };
+        '';
+      };
+    };
+    mysql = { config, pkgs, ... }: {
+      networking.firewall.enable = false;
+      services.mysql = {
+        enable = true;
+        initialScript = pkgs.writeText "mysql_init.sql" ''
+          CREATE DATABASE prosody;
+          CREATE USER 'prosody'@'server' IDENTIFIED BY 'password123';
+          GRANT ALL PRIVILEGES ON prosody.* TO 'prosody'@'server';
+          FLUSH PRIVILEGES;
+        '';
+        package = pkgs.mariadb;
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    # Check with mysql storage
+    mysql.wait_for_unit("mysql.service")
+    server.wait_for_unit("prosody.service")
+    server.succeed('prosodyctl status | grep "Prosody is running"')
+
+    server.succeed("create-prosody-users")
+    client.succeed("send-message")
+    server.succeed("delete-prosody-users")
+  '';
+}
diff --git a/nixos/tests/xmpp/prosody.nix b/nixos/tests/xmpp/prosody.nix
index c343b580879a4..14eab56fb821f 100644
--- a/nixos/tests/xmpp/prosody.nix
+++ b/nixos/tests/xmpp/prosody.nix
@@ -81,6 +81,7 @@ in import ../make-test-python.nix {
   };
 
   testScript = { nodes, ... }: ''
+    # Check with sqlite storage
     server.wait_for_unit("prosody.service")
     server.succeed('prosodyctl status | grep "Prosody is running"')
 
diff --git a/nixos/tests/xmpp/xmpp-sendmessage.nix b/nixos/tests/xmpp/xmpp-sendmessage.nix
index 47a77f524c6af..80dfcff2d0ebc 100644
--- a/nixos/tests/xmpp/xmpp-sendmessage.nix
+++ b/nixos/tests/xmpp/xmpp-sendmessage.nix
@@ -51,11 +51,8 @@ class CthonTest(ClientXMPP):
         log.info('Message sent')
 
         # Test http upload (XEP_0363)
-        def timeout_callback(arg):
-            log.error("ERROR: Cannot upload file. XEP_0363 seems broken")
-            sys.exit(1)
         try:
-            url = await self['xep_0363'].upload_file("${dummyFile}",timeout=10, timeout_callback=timeout_callback)
+            url = await self['xep_0363'].upload_file("${dummyFile}",timeout=10)
         except:
             log.error("ERROR: Cannot run upload command. XEP_0363 seems broken")
             sys.exit(1)
diff --git a/nixos/tests/xrdp.nix b/nixos/tests/xrdp.nix
index 0e1d521c5aced..f277d4b795256 100644
--- a/nixos/tests/xrdp.nix
+++ b/nixos/tests/xrdp.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "xrdp";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ volth ];
+    maintainers = [ ];
   };
 
   nodes = {
diff --git a/nixos/tests/xterm.nix b/nixos/tests/xterm.nix
index 4ee31139ab52b..745d33e8a0d53 100644
--- a/nixos/tests/xterm.nix
+++ b/nixos/tests/xterm.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ nequissimus ];
   };
 
-  machine = { pkgs, ... }:
+  nodes.machine = { pkgs, ... }:
     {
       imports = [ ./common/x11.nix ];
       services.xserver.desktopManager.xterm.enable = false;
diff --git a/nixos/tests/yabar.nix b/nixos/tests/yabar.nix
index c2431e556c378..ff7a47ae63709 100644
--- a/nixos/tests/yabar.nix
+++ b/nixos/tests/yabar.nix
@@ -8,7 +8,7 @@ with lib;
     maintainers = [ ];
   };
 
-  machine = {
+  nodes.machine = {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
 
     test-support.displayManager.auto.user = "bob";
diff --git a/nixos/tests/zammad.nix b/nixos/tests/zammad.nix
new file mode 100644
index 0000000000000..4e466f6e3b9bf
--- /dev/null
+++ b/nixos/tests/zammad.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix (
+  { lib, pkgs, ... }:
+
+  {
+    name = "zammad";
+
+    meta.maintainers = with lib.maintainers; [ garbas taeer ];
+
+    nodes.machine = { config, ... }: {
+      services.zammad.enable = true;
+      services.zammad.secretKeyBaseFile = pkgs.writeText "secret" ''
+        52882ef142066e09ab99ce816ba72522e789505caba224a52d750ec7dc872c2c371b2fd19f16b25dfbdd435a4dd46cb3df9f82eb63fafad715056bdfe25740d6
+      '';
+
+      systemd.services.zammad-locale-cheat =
+        let cfg = config.services.zammad; in
+        {
+          serviceConfig = {
+            Type = "simple";
+            Restart = "always";
+
+            User = "zammad";
+            Group = "zammad";
+            PrivateTmp = true;
+            StateDirectory = "zammad";
+            WorkingDirectory = cfg.dataDir;
+          };
+          wantedBy = [ "zammad-web.service" ];
+          description = "Hack in the locale files so zammad doesn't try to access the internet";
+          script = ''
+            mkdir -p ./config/translations
+            VERSION=$(cat ${cfg.package}/VERSION)
+
+            # If these files are not in place, zammad will try to access the internet.
+            # For the test, we only need to supply en-us.
+            echo '[{"locale":"en-us","alias":"en","name":"English (United States)","active":true,"dir":"ltr"}]' \
+              > ./config/locales-$VERSION.yml
+            echo '[{"locale":"en-us","format":"time","source":"date","target":"mm/dd/yyyy","target_initial":"mm/dd/yyyy"},{"locale":"en-us","format":"time","source":"timestamp","target":"mm/dd/yyyy HH:MM","target_initial":"mm/dd/yyyy HH:MM"}]' \
+              > ./config/translations/en-us-$VERSION.yml
+          '';
+        };
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("postgresql.service")
+      machine.wait_for_unit("zammad-web.service")
+      machine.wait_for_unit("zammad-websocket.service")
+      machine.wait_for_unit("zammad-scheduler.service")
+      # wait for zammad to fully come up
+      machine.sleep(120)
+
+      # without the grep the command does not produce valid utf-8 for some reason
+      with subtest("welcome screen loads"):
+          machine.succeed(
+              "curl -sSfL http://localhost:3000/ | grep '<title>Zammad Helpdesk</title>'"
+          )
+    '';
+  }
+)
diff --git a/nixos/tests/zeronet-conservancy.nix b/nixos/tests/zeronet-conservancy.nix
new file mode 100644
index 0000000000000..8cb649cbdaabc
--- /dev/null
+++ b/nixos/tests/zeronet-conservancy.nix
@@ -0,0 +1,25 @@
+let
+  port = 43110;
+in
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "zeronet-conservancy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  nodes.machine = { config, pkgs, ... }: {
+    services.zeronet = {
+      enable = true;
+      package = pkgs.zeronet-conservancy;
+      inherit port;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("zeronet.service")
+
+    machine.wait_for_open_port(${toString port})
+
+    machine.succeed("curl --fail -H 'Accept: text/html, application/xml, */*' localhost:${toString port}/Stats")
+  '';
+})
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
index d25090403e5fa..0b44961a3deb5 100644
--- a/nixos/tests/zfs.nix
+++ b/nixos/tests/zfs.nix
@@ -18,7 +18,7 @@ let
         maintainers = [ adisbladis ];
       };
 
-      machine = { pkgs, lib, ... }:
+      nodes.machine = { pkgs, lib, ... }:
         let
           usersharePath = "/var/lib/samba/usershares";
         in {
@@ -127,4 +127,54 @@ in {
   };
 
   installer = (import ./installer.nix { }).zfsroot;
+
+  expand-partitions = makeTest {
+    name = "multi-disk-zfs";
+    nodes = {
+      machine = { pkgs, ... }: {
+        environment.systemPackages = [ pkgs.parted ];
+        boot.supportedFilesystems = [ "zfs" ];
+        networking.hostId = "00000000";
+
+        virtualisation = {
+          emptyDiskImages = [ 20480 20480 20480 20480 20480 20480 ];
+        };
+
+        specialisation.resize.configuration = {
+          services.zfs.expandOnBoot = [ "tank" ];
+        };
+      };
+    };
+
+    testScript = { nodes, ... }:
+      ''
+        start_all()
+        machine.wait_for_unit("default.target")
+        print(machine.succeed('mount'))
+
+        print(machine.succeed('parted --script /dev/vdb -- mklabel gpt'))
+        print(machine.succeed('parted --script /dev/vdb -- mkpart primary 1M 70M'))
+
+        print(machine.succeed('parted --script /dev/vdc -- mklabel gpt'))
+        print(machine.succeed('parted --script /dev/vdc -- mkpart primary 1M 70M'))
+
+        print(machine.succeed('zpool create tank mirror /dev/vdb1 /dev/vdc1 mirror /dev/vdd /dev/vde mirror /dev/vdf /dev/vdg'))
+        print(machine.succeed('zpool list -v'))
+        print(machine.succeed('mount'))
+        start_size = int(machine.succeed('df -k --output=size /tank | tail -n1').strip())
+
+        print(machine.succeed("/run/current-system/specialisation/resize/bin/switch-to-configuration test >&2"))
+        machine.wait_for_unit("zpool-expand-pools.service")
+        machine.wait_for_unit("zpool-expand@tank.service")
+
+        print(machine.succeed('zpool list -v'))
+        new_size = int(machine.succeed('df -k --output=size /tank | tail -n1').strip())
+
+        if (new_size - start_size) > 20000000:
+          print("Disk grew appropriately.")
+        else:
+          print(f"Disk went from {start_size} to {new_size}, which doesn't seem right.")
+          exit(1)
+      '';
+  };
 }
diff --git a/nixos/tests/zigbee2mqtt.nix b/nixos/tests/zigbee2mqtt.nix
index 98aadbb699bdf..1592202fb3a76 100644
--- a/nixos/tests/zigbee2mqtt.nix
+++ b/nixos/tests/zigbee2mqtt.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, lib, ... }:
 
   {
-    machine = { pkgs, ... }:
+    nodes.machine = { pkgs, ... }:
       {
         services.zigbee2mqtt = {
           enable = true;
diff --git a/nixos/tests/zoneminder.nix b/nixos/tests/zoneminder.nix
index a4e1a05ec0ee4..3c97bc8282d22 100644
--- a/nixos/tests/zoneminder.nix
+++ b/nixos/tests/zoneminder.nix
@@ -4,7 +4,7 @@ import ./make-test-python.nix ({ lib, ...}:
   name = "zoneminder";
   meta.maintainers = with lib.maintainers; [ danielfullmer ];
 
-  machine = { ... }:
+  nodes.machine = { ... }:
   {
     services.zoneminder = {
       enable = true;
diff --git a/nixos/tests/zrepl.nix b/nixos/tests/zrepl.nix
new file mode 100644
index 0000000000000..85dd834a6aafb
--- /dev/null
+++ b/nixos/tests/zrepl.nix
@@ -0,0 +1,66 @@
+import ./make-test-python.nix (
+  {
+    nodes.host = {config, pkgs, ...}: {
+      config = {
+        # Prerequisites for ZFS and tests.
+        boot.supportedFilesystems = [ "zfs" ];
+        environment.systemPackages = [ pkgs.zrepl ];
+        networking.hostId = "deadbeef";
+        services.zrepl = {
+          enable = true;
+          settings = {
+            # Enable Prometheus output for status assertions.
+            global.monitoring = [{
+              type = "prometheus";
+              listen = ":9811";
+            }];
+            # Create a periodic snapshot job for an ephemeral zpool.
+            jobs = [{
+              name = "snap_test";
+              type = "snap";
+
+              filesystems."test" = true;
+              snapshotting = {
+                type = "periodic";
+                prefix = "zrepl_";
+                interval = "1s";
+              };
+
+              pruning.keep = [{
+                type = "last_n";
+                count = 8;
+              }];
+            }];
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      with subtest("Wait for zrepl and network ready"):
+          host.wait_for_unit("network-online.target")
+          host.wait_for_unit("zrepl.service")
+
+      with subtest("Create test zpool"):
+          # ZFS requires 64MiB minimum pool size.
+          host.succeed("fallocate -l 64MiB /root/zpool.img")
+          host.succeed("zpool create test /root/zpool.img")
+
+      with subtest("Check for completed zrepl snapshot"):
+          # zrepl periodic snapshot job creates a snapshot with this prefix.
+          host.wait_until_succeeds("zfs list -t snapshot | grep -q zrepl_")
+
+      with subtest("Verify HTTP monitoring server is configured"):
+          out = host.succeed("curl -f localhost:9811/metrics")
+
+          assert (
+              "zrepl_version_daemon" in out
+          ), "zrepl version metric was not found in Prometheus output"
+
+          assert (
+              "zrepl_zfs_snapshot_duration_count{filesystem=\"test\"}" in out
+          ), "zrepl snapshot counter for test was not found in Prometheus output"
+    '';
+  })
diff --git a/nixos/tests/zsh-history.nix b/nixos/tests/zsh-history.nix
index 355687798406b..64f32a07e2158 100644
--- a/nixos/tests/zsh-history.nix
+++ b/nixos/tests/zsh-history.nix
@@ -21,13 +21,13 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     default.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
 
     # Login
-    default.wait_until_tty_matches(1, "login: ")
+    default.wait_until_tty_matches("1", "login: ")
     default.send_chars("root\n")
-    default.wait_until_tty_matches(1, r"\nroot@default\b")
+    default.wait_until_tty_matches("1", r"\nroot@default\b")
 
     # Generate some history
     default.send_chars("echo foobar\n")
-    default.wait_until_tty_matches(1, "foobar")
+    default.wait_until_tty_matches("1", "foobar")
 
     # Ensure that command was recorded in history
     default.succeed("/run/current-system/sw/bin/history list | grep -q foobar")